package archive

import (
	"container/list"
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"sort"
	"sync"
	"time"

	"git.mleku.dev/mleku/nostr/encoders/filter"
)

// QueryCache tracks which filters have been queried recently to avoid
// repeated requests to archive relays for the same filter.
type QueryCache struct {
	mu       sync.RWMutex
	entries  map[string]*list.Element
	order    *list.List
	maxSize  int
	ttl      time.Duration
}

// queryCacheEntry holds a cached query fingerprint and timestamp.
type queryCacheEntry struct {
	fingerprint string
	queriedAt   time.Time
}

// NewQueryCache creates a new query cache.
func NewQueryCache(ttl time.Duration, maxSize int) *QueryCache {
	if maxSize <= 0 {
		maxSize = 100000
	}
	if ttl <= 0 {
		ttl = 24 * time.Hour
	}

	return &QueryCache{
		entries: make(map[string]*list.Element),
		order:   list.New(),
		maxSize: maxSize,
		ttl:     ttl,
	}
}

// HasQueried returns true if the filter was queried within the TTL.
func (qc *QueryCache) HasQueried(f *filter.F) bool {
	fingerprint := qc.normalizeAndHash(f)

	qc.mu.RLock()
	elem, exists := qc.entries[fingerprint]
	qc.mu.RUnlock()

	if !exists {
		return false
	}

	entry := elem.Value.(*queryCacheEntry)

	// Check if still within TTL
	if time.Since(entry.queriedAt) > qc.ttl {
		// Expired - remove it
		qc.mu.Lock()
		if elem, exists := qc.entries[fingerprint]; exists {
			delete(qc.entries, fingerprint)
			qc.order.Remove(elem)
		}
		qc.mu.Unlock()
		return false
	}

	return true
}

// MarkQueried marks a filter as having been queried.
func (qc *QueryCache) MarkQueried(f *filter.F) {
	fingerprint := qc.normalizeAndHash(f)

	qc.mu.Lock()
	defer qc.mu.Unlock()

	// Update existing entry
	if elem, exists := qc.entries[fingerprint]; exists {
		qc.order.MoveToFront(elem)
		elem.Value.(*queryCacheEntry).queriedAt = time.Now()
		return
	}

	// Evict oldest if at capacity
	if len(qc.entries) >= qc.maxSize {
		oldest := qc.order.Back()
		if oldest != nil {
			entry := oldest.Value.(*queryCacheEntry)
			delete(qc.entries, entry.fingerprint)
			qc.order.Remove(oldest)
		}
	}

	// Add new entry
	entry := &queryCacheEntry{
		fingerprint: fingerprint,
		queriedAt:   time.Now(),
	}
	elem := qc.order.PushFront(entry)
	qc.entries[fingerprint] = elem
}

// normalizeAndHash creates a canonical fingerprint for a filter.
// This ensures that differently-ordered filters with the same content
// produce identical fingerprints.
func (qc *QueryCache) normalizeAndHash(f *filter.F) string {
	h := sha256.New()

	// Normalize and hash IDs (sorted)
	if f.Ids != nil && f.Ids.Len() > 0 {
		ids := make([]string, 0, f.Ids.Len())
		for _, id := range f.Ids.T {
			ids = append(ids, string(id))
		}
		sort.Strings(ids)
		h.Write([]byte("ids:"))
		for _, id := range ids {
			h.Write([]byte(id))
		}
	}

	// Normalize and hash Authors (sorted)
	if f.Authors != nil && f.Authors.Len() > 0 {
		authors := make([]string, 0, f.Authors.Len())
		for _, author := range f.Authors.T {
			authors = append(authors, string(author))
		}
		sort.Strings(authors)
		h.Write([]byte("authors:"))
		for _, a := range authors {
			h.Write([]byte(a))
		}
	}

	// Normalize and hash Kinds (sorted)
	if f.Kinds != nil && f.Kinds.Len() > 0 {
		kinds := f.Kinds.ToUint16()
		sort.Slice(kinds, func(i, j int) bool { return kinds[i] < kinds[j] })
		h.Write([]byte("kinds:"))
		for _, k := range kinds {
			var buf [2]byte
			binary.BigEndian.PutUint16(buf[:], k)
			h.Write(buf[:])
		}
	}

	// Normalize and hash Tags (sorted by key, then values)
	if f.Tags != nil && f.Tags.Len() > 0 {
		// Collect all tag keys and sort them
		tagMap := make(map[string][]string)
		for _, t := range *f.Tags {
			if t.Len() > 0 {
				key := string(t.Key())
				values := make([]string, 0, t.Len()-1)
				for j := 1; j < t.Len(); j++ {
					values = append(values, string(t.T[j]))
				}
				sort.Strings(values)
				tagMap[key] = values
			}
		}

		// Sort keys and hash
		keys := make([]string, 0, len(tagMap))
		for k := range tagMap {
			keys = append(keys, k)
		}
		sort.Strings(keys)

		h.Write([]byte("tags:"))
		for _, k := range keys {
			h.Write([]byte(k))
			h.Write([]byte(":"))
			for _, v := range tagMap[k] {
				h.Write([]byte(v))
			}
		}
	}

	// Hash Since timestamp
	if f.Since != nil {
		h.Write([]byte("since:"))
		var buf [8]byte
		binary.BigEndian.PutUint64(buf[:], uint64(f.Since.V))
		h.Write(buf[:])
	}

	// Hash Until timestamp
	if f.Until != nil {
		h.Write([]byte("until:"))
		var buf [8]byte
		binary.BigEndian.PutUint64(buf[:], uint64(f.Until.V))
		h.Write(buf[:])
	}

	// Hash Limit
	if f.Limit != nil && *f.Limit > 0 {
		h.Write([]byte("limit:"))
		var buf [4]byte
		binary.BigEndian.PutUint32(buf[:], uint32(*f.Limit))
		h.Write(buf[:])
	}

	// Hash Search (NIP-50)
	if len(f.Search) > 0 {
		h.Write([]byte("search:"))
		h.Write(f.Search)
	}

	return hex.EncodeToString(h.Sum(nil))
}

// Len returns the number of cached queries.
func (qc *QueryCache) Len() int {
	qc.mu.RLock()
	defer qc.mu.RUnlock()
	return len(qc.entries)
}

// MaxSize returns the maximum cache size.
func (qc *QueryCache) MaxSize() int {
	return qc.maxSize
}

// Clear removes all entries from the cache.
func (qc *QueryCache) Clear() {
	qc.mu.Lock()
	defer qc.mu.Unlock()
	qc.entries = make(map[string]*list.Element)
	qc.order.Init()
}
