package cli

import (
	"context"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/alecthomas/kingpin"
	"github.com/pkg/errors"

	"github.com/kopia/kopia/internal/clock"
	"github.com/kopia/kopia/repo/blob"
	"github.com/kopia/kopia/repo/content"
)

type logSessionInfo struct {
	id        string
	startTime time.Time
	endTime   time.Time
	segments  []blob.Metadata
	totalSize int64
}

type logSelectionCriteria struct {
	all         bool
	latest      int
	youngerThan time.Duration
	olderThan   time.Duration
}

func (c *logSelectionCriteria) setup(cmd *kingpin.CmdClause) {
	cmd.Flag("all", "Show all logs").BoolVar(&c.all)
	cmd.Flag("latest", "Include last N logs").Short('n').IntVar(&c.latest)
	cmd.Flag("younger-than", "Include logs younger than X (e.g. '1h')").DurationVar(&c.youngerThan)
	cmd.Flag("older-than", "Include logs older than X (e.g. '1h')").DurationVar(&c.olderThan)
}

func (c *logSelectionCriteria) any() bool {
	return c.all || c.latest > 0 || c.youngerThan > 0 || c.olderThan > 0
}

func (c *logSelectionCriteria) filterLogSessions(allSessions []*logSessionInfo) []*logSessionInfo {
	if c.all {
		return allSessions
	}

	if c.youngerThan > 0 {
		allSessions = filterLogSessions(allSessions, func(ls *logSessionInfo) bool {
			return clock.Now().Sub(ls.startTime) < c.youngerThan
		})
	}

	if c.olderThan > 0 {
		allSessions = filterLogSessions(allSessions, func(ls *logSessionInfo) bool {
			return clock.Now().Sub(ls.startTime) > c.olderThan
		})
	}

	if c.latest > 0 && len(allSessions) > c.latest {
		allSessions = allSessions[len(allSessions)-c.latest:]
	}

	return allSessions
}

func getLogSessions(ctx context.Context, st blob.Reader) ([]*logSessionInfo, error) {
	sessions := map[string]*logSessionInfo{}

	var allSessions []*logSessionInfo

	if err := st.ListBlobs(ctx, content.TextLogBlobPrefix, func(bm blob.Metadata) error {
		parts := strings.Split(string(bm.BlobID), "_")

		// nolint:gomnd
		if len(parts) < 8 {
			log(ctx).Errorf("invalid part count: %v skipping unrecognized log: %v", len(parts), bm.BlobID)
			return nil
		}

		id := parts[2] + "_" + parts[3]

		// nolint:gomnd
		startTime, err := strconv.ParseInt(parts[4], 10, 64)
		if err != nil {
			log(ctx).Errorf("invalid start time - skipping unrecognized log: %v", bm.BlobID)

			// nolint:nilerr
			return nil
		}

		// nolint:gomnd
		endTime, err := strconv.ParseInt(parts[5], 10, 64)
		if err != nil {
			log(ctx).Errorf("invalid end time - skipping unrecognized log: %v", bm.BlobID)

			// nolint:nilerr
			return nil
		}

		s := sessions[id]
		if s == nil {
			s = &logSessionInfo{
				id: id,
			}
			sessions[id] = s
			allSessions = append(allSessions, s)
		}

		if t := time.Unix(startTime, 0); s.startTime.IsZero() || t.Before(s.startTime) {
			s.startTime = t
		}

		if t := time.Unix(endTime, 0); t.After(s.endTime) {
			s.endTime = t
		}

		s.segments = append(s.segments, bm)
		s.totalSize += bm.Length

		return nil
	}); err != nil {
		return nil, errors.Wrap(err, "error listing logs")
	}

	for _, s := range allSessions {
		sort.Slice(s.segments, func(i, j int) bool {
			return s.segments[i].Timestamp.Before(s.segments[j].Timestamp)
		})
	}

	// sort sessions by start time
	sort.Slice(allSessions, func(i, j int) bool {
		return allSessions[i].segments[0].Timestamp.Before(allSessions[j].segments[0].Timestamp)
	})

	return allSessions, nil
}

func filterLogSessions(logs []*logSessionInfo, predicate func(l *logSessionInfo) bool) []*logSessionInfo {
	var result []*logSessionInfo

	for _, l := range logs {
		if predicate(l) {
			result = append(result, l)
		}
	}

	return result
}
