package sdk

import (
	"context"
	"slices"
	"sync"
	"sync/atomic"
	"time"

	"fiatjaf.com/nostr"
	"fiatjaf.com/nostr/sdk/cache"
)

type GenericList[V comparable, I TagItemWithValue[V]] struct {
	PubKey nostr.PubKey `json:"-"` // must always be set otherwise things will break
	Event  *nostr.Event `json:"-"` // may be empty if a contact list event wasn't found

	Items []I
}

type TagItemWithValue[V comparable] interface {
	Value() V
}

var (
	genericListMutexes = [60]sync.Mutex{}
	valueWasJustCached = [60]atomic.Bool{}
)

func fetchGenericList[V comparable, I TagItemWithValue[V]](
	sys *System,
	ctx context.Context,
	pubkey nostr.PubKey,
	actualKind nostr.Kind,
	replaceableIndex replaceableIndex,
	parseTag func(nostr.Tag) (I, bool),
	cache cache.Cache32[GenericList[V, I]],
) (fl GenericList[V, I], fromInternal bool) {
	// we have 60 mutexes, so we can load up to 60 lists at the same time, but if we do the same exact
	// call that will do it only once, the subsequent ones will wait for a result to be cached
	// and then return it from cache -- 13 is an arbitrary index for the pubkey
	n := pubkey[7]
	lockIdx := (nostr.Kind(n) + actualKind) % 60
	genericListMutexes[lockIdx].Lock()

	if valueWasJustCached[lockIdx].CompareAndSwap(true, false) {
		// this ensures the cache has had time to commit the values
		// so we don't repeat a fetch immediately after the other
		time.Sleep(time.Millisecond * 10)
	}

	genericListMutexes[lockIdx].Unlock()

	if v, ok := cache.Get(pubkey); ok {
		return v, true
	}

	v := GenericList[V, I]{PubKey: pubkey}

	for evt := range sys.Store.QueryEvents(nostr.Filter{
		Kinds:   []nostr.Kind{actualKind},
		Authors: []nostr.PubKey{pubkey},
	}, 1) {
		// ok, we found something locally
		items := parseItemsFromEventTags(evt, parseTag)
		v.Event = &evt
		v.Items = items

		// but if we haven't tried fetching from the network recently we should do it
		lastFetchKey := makeLastFetchKey(actualKind, pubkey)
		lastFetchData, _ := sys.KVStore.Get(lastFetchKey)
		if lastFetchData == nil || nostr.Now()-decodeTimestamp(lastFetchData) > getLocalStoreRefreshDaysForKind(actualKind)*24*60*60 {
			newV := tryFetchListFromNetwork(ctx, sys, pubkey, replaceableIndex, parseTag)
			if newV != nil && newV.Event.CreatedAt > v.Event.CreatedAt {
				v = *newV
				sys.Store.ReplaceEvent(*v.Event)
			}

			// register this even if we didn't find anything because we tried
			// (and we still have the previous event in our local store)
			sys.KVStore.Set(lastFetchKey, encodeTimestamp(nostr.Now()))
		}

		// and finally save this to cache
		cache.SetWithTTL(pubkey, v, time.Hour*6)
		valueWasJustCached[lockIdx].Store(true)

		return v, true
	}

	if newV := tryFetchListFromNetwork(ctx, sys, pubkey, replaceableIndex, parseTag); newV != nil {
		v = *newV

		// we'll only save this if we got something which means we found at least one event
		lastFetchKey := makeLastFetchKey(actualKind, pubkey)
		sys.KVStore.Set(lastFetchKey, encodeTimestamp(nostr.Now()))
		sys.Store.SaveEvent(*v.Event)
	}

	// save cache even if we didn't get anything
	cache.SetWithTTL(pubkey, v, time.Hour*6)
	valueWasJustCached[lockIdx].Store(true)

	return v, false
}

func tryFetchListFromNetwork[V comparable, I TagItemWithValue[V]](
	ctx context.Context,
	sys *System,
	pubkey nostr.PubKey,
	replaceableIndex replaceableIndex,
	parseTag func(nostr.Tag) (I, bool),
) *GenericList[V, I] {
	evt, err := sys.replaceableLoaders[replaceableIndex].Load(ctx, pubkey)
	if err != nil {
		return nil
	}

	v := &GenericList[V, I]{
		PubKey: pubkey,
		Event:  &evt,
		Items:  parseItemsFromEventTags(evt, parseTag),
	}
	sys.Publisher.Publish(ctx, evt)

	return v
}

func parseItemsFromEventTags[V comparable, I TagItemWithValue[V]](
	evt nostr.Event,
	parseTag func(nostr.Tag) (I, bool),
) []I {
	result := make([]I, 0, len(evt.Tags))
	for _, tag := range evt.Tags {
		item, ok := parseTag(tag)
		if ok {
			// check if this already exists before adding
			if slices.IndexFunc(result, func(i I) bool { return i.Value() == item.Value() }) == -1 {
				result = append(result, item)
			}
		}
	}
	return result
}

func getLocalStoreRefreshDaysForKind(kind nostr.Kind) nostr.Timestamp {
	switch kind {
	case 0:
		return 7
	case 3:
		return 1
	default:
		return 3
	}
}
