package qbittorrent

import (
	"context"
	"fmt"
	"slices"
	"time"

	"git.kmsign.ru/royalcat/tstor/plugins/qbittorrent/pkg/qbittorrent"
	"github.com/hashicorp/golang-lru/v2/expirable"
	"github.com/royalcat/btrgo/btrsync"
	"github.com/viccon/sturdyc"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/metric"
)

var meter = otel.Meter("git.kmsign.ru/royalcat/tstor/plugins/qbittorrent")

type cacheClient struct {
	qb qbittorrent.Client

	propertiesCache *expirable.LRU[string, qbittorrent.TorrentProperties]

	pieceCache btrsync.MapOf[pieceKey, int]

	infoClient *sturdyc.Client[*qbittorrent.TorrentInfo]
}

type pieceKey struct {
	hash  string
	index int
}

func wrapClient(qb qbittorrent.Client) *cacheClient {
	const (
		cacheSize = 5000
		cacheTTL  = time.Minute
	)

	return &cacheClient{
		qb:              qb,
		propertiesCache: expirable.NewLRU[string, qbittorrent.TorrentProperties](cacheSize, nil, cacheTTL),

		infoClient: sturdyc.New[*qbittorrent.TorrentInfo](cacheSize, 1, cacheTTL, 10,
			sturdyc.WithEarlyRefreshes(time.Minute, time.Minute*5, time.Second*10, time.Second*10),
			sturdyc.WithRefreshCoalescing(100, time.Second/4),
			sturdyc.WithMetrics(newSturdycMetrics()),
		),

		pieceCache: btrsync.MapOf[pieceKey, int]{},
	}
}

func (f *cacheClient) getInfo(ctx context.Context, hash string) (*qbittorrent.TorrentInfo, error) {
	out, err := f.infoClient.GetOrFetchBatch(ctx, []string{hash},
		f.infoClient.BatchKeyFn(""),
		func(ctx context.Context, ids []string) (map[string]*qbittorrent.TorrentInfo, error) {
			infos, err := f.qb.Torrent().GetTorrents(ctx, &qbittorrent.TorrentOption{
				Hashes: ids,
			})
			if err != nil {
				return nil, fmt.Errorf("error to get torrents: %w", err)
			}

			out := make(map[string]*qbittorrent.TorrentInfo)
			for _, info := range infos {
				out[info.Hash] = info
			}

			return out, nil
		},
	)
	if err != nil {
		return nil, err
	}

	if out[hash] == nil {
		return nil, nil
	}

	return out[hash], nil
}

func (f *cacheClient) getProperties(ctx context.Context, hash string) (*qbittorrent.TorrentProperties, error) {
	if v, ok := f.propertiesCache.Get(hash); ok {
		return &v, nil
	}

	info, err := f.qb.Torrent().GetProperties(ctx, hash)
	if err != nil {
		return nil, err
	}

	f.propertiesCache.Add(hash, *info)
	return info, nil
}

func (f *cacheClient) listContent(ctx context.Context, hash string) ([]*qbittorrent.TorrentContent, error) {
	contents, err := f.qb.Torrent().GetContents(ctx, hash)
	if err != nil {
		return nil, err
	}
	return contents, nil
}

func (f *cacheClient) getContent(ctx context.Context, hash string, contentIndex int) (*qbittorrent.TorrentContent, error) {
	contents, err := f.qb.Torrent().GetContents(ctx, hash, contentIndex)
	if err != nil {
		return nil, err
	}

	contentI := slices.IndexFunc(contents, func(c *qbittorrent.TorrentContent) bool {
		return c.Index == contentIndex
	})
	if contentI == -1 {
		return nil, fmt.Errorf("content not found")
	}

	return contents[contentI], nil
}

func (f *cacheClient) isPieceComplete(ctx context.Context, hash string, pieceIndex int) (bool, error) {
	cachedPieceState, ok := f.pieceCache.Load(pieceKey{hash: hash, index: pieceIndex})
	if ok && cachedPieceState == 2 {
		return true, nil
	}

	completion, err := f.qb.Torrent().GetPiecesStates(ctx, hash)
	if err != nil {
		return false, err
	}

	for i, v := range completion {
		f.pieceCache.Store(pieceKey{hash: hash, index: i}, v)
	}

	if completion[pieceIndex] == 2 {
		return true, nil
	}

	return false, nil
}

func (f *cacheClient) waitPieceToComplete(ctx context.Context, hash string, pieceIndex int) error {
	const checkingInterval = 1 * time.Second

	ok, err := f.isPieceComplete(ctx, hash, pieceIndex)
	if err != nil {
		return err
	}
	if ok {
		return nil
	}

	if deadline, ok := ctx.Deadline(); ok && time.Until(deadline) < checkingInterval {
		return context.DeadlineExceeded
	}

	ticker := time.NewTicker(checkingInterval)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-ticker.C:
			ok, err := f.isPieceComplete(ctx, hash, pieceIndex)
			if err != nil {
				return err
			}
			if ok {
				return nil
			}

			if deadline, ok := ctx.Deadline(); ok && time.Until(deadline) < checkingInterval {
				return context.DeadlineExceeded
			}
		}
	}
}

type sturdycMetrics struct {
	ctx context.Context

	cacheHit       metric.Int64Counter
	cacheMiss      metric.Int64Counter
	refresh        metric.Int64Counter
	missing        metric.Int64Counter
	forcedEviction metric.Int64Counter
	entryEviction  metric.Int64Counter
	batchSize      metric.Int64Histogram

	observeCacheSize func() int
}

var _ sturdyc.MetricsRecorder = (*sturdycMetrics)(nil)

func newSturdycMetrics() *sturdycMetrics {
	m := &sturdycMetrics{
		ctx:            context.Background(),
		cacheHit:       must(meter.Int64Counter("sturdyc_cache_hit")),
		cacheMiss:      must(meter.Int64Counter("sturdyc_cache_miss")),
		refresh:        must(meter.Int64Counter("sturdyc_cache_refresh")),
		missing:        must(meter.Int64Counter("sturdyc_cache_missing")),
		forcedEviction: must(meter.Int64Counter("sturdyc_cache_forced_eviction")),
		entryEviction:  must(meter.Int64Counter("sturdyc_cache_entry_eviction")),
		batchSize:      must(meter.Int64Histogram("sturdyc_cache_batch_size")),
	}

	must(meter.Int64ObservableGauge("sturdyc_cache_size",
		metric.WithInt64Callback(func(ctx context.Context, io metric.Int64Observer) error {
			if m.observeCacheSize == nil {
				return nil
			}
			io.Observe(int64(m.observeCacheSize()))
			return nil
		})))

	return m
}

func (s *sturdycMetrics) CacheHit() {
	s.cacheHit.Add(s.ctx, 1)
}

func (s *sturdycMetrics) CacheMiss() {
	s.cacheMiss.Add(s.ctx, 1)
}

func (s *sturdycMetrics) Refresh() {
	s.refresh.Add(s.ctx, 1)
}

func (s *sturdycMetrics) MissingRecord() {
	s.missing.Add(s.ctx, 1)
}

func (s *sturdycMetrics) ForcedEviction() {
	s.forcedEviction.Add(s.ctx, 1)
}

func (s *sturdycMetrics) CacheBatchRefreshSize(size int) {
	s.batchSize.Record(s.ctx, int64(size))
}

func (s *sturdycMetrics) ObserveCacheSize(callback func() int) {
	s.observeCacheSize = callback
}

func (s *sturdycMetrics) EntriesEvicted(evictd int) {
	s.entryEviction.Add(s.ctx, int64(evictd))
}

// AsynchronousRefresh implements sturdyc.MetricsRecorder.
func (s *sturdycMetrics) AsynchronousRefresh() {
	return
}

// SynchronousRefresh implements sturdyc.MetricsRecorder.
func (s *sturdycMetrics) SynchronousRefresh() {
	return
}

func (s *sturdycMetrics) ShardIndex(int) {
	return
}

func must[T any](v T, err error) T {
	if err != nil {
		panic(err)
	}
	return v
}