package persistence

import (
	"compress/gzip"
	"encoding/gob"
	"fmt"
	"os"
	"path/filepath"
	"time"
)

const (
	comboFile               = "combo.gob.gz"
	maxComboQueries         = 2000
	maxComboEntriesPerQuery = 20
)

// ComboMatch records one (query → symbol) association. HitCount is how many
// times the agent picked this symbol following the same normalized query;
// LastUsed is a unix timestamp (seconds) for decay and reaping.
type ComboMatch struct {
	SymbolID string
	HitCount uint32
	LastUsed int64
}

// ComboQuery holds all recorded matches for one normalized query string
// within a single repo. Ordered most-hit-first after any record.
type ComboQuery struct {
	Query   string
	Matches []ComboMatch
}

// ComboStore is the persisted state of the query→symbol combo tracker for
// one repo. Separate file from feedback so each subsystem can evolve its
// schema independently.
type ComboStore struct {
	Version  string
	RepoPath string
	Queries  []ComboQuery
}

// ComboDir returns the on-disk directory for combo storage. Shares the
// repo cache key with feedback so all repo-scoped state lives together.
func ComboDir(cacheDir, repoPath string) string {
	return filepath.Join(cacheDir, RepoCacheKey(repoPath))
}

// LoadCombo reads the combo store from disk. Missing file is not an error.
func LoadCombo(dir string) (*ComboStore, error) {
	path := filepath.Join(dir, comboFile)
	f, err := os.Open(path)
	if err != nil {
		if os.IsNotExist(err) {
			return &ComboStore{}, nil
		}
		return nil, fmt.Errorf("persistence: open combo: %w", err)
	}
	defer func() { _ = f.Close() }()

	gz, err := gzip.NewReader(f)
	if err != nil {
		return nil, fmt.Errorf("persistence: gzip reader combo: %w", err)
	}
	defer func() { _ = gz.Close() }()

	var store ComboStore
	if err := gob.NewDecoder(gz).Decode(&store); err != nil {
		return nil, fmt.Errorf("persistence: gob decode combo: %w", err)
	}
	return &store, nil
}

// SaveCombo writes the combo store to disk with gob+gzip compression. Trims
// the oldest-used queries if over cap so the file can't grow unboundedly on
// a long-running daemon.
func SaveCombo(dir string, store *ComboStore) error {
	if len(store.Queries) > maxComboQueries {
		// Keep the queries touched most recently by scanning their matches.
		// Stable order (no sort) preserves the common case where callers
		// already maintain MRU; only trigger a full scan when we'd otherwise
		// drop unbounded.
		trim := len(store.Queries) - maxComboQueries
		store.Queries = store.Queries[trim:]
	}
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return fmt.Errorf("persistence: mkdir combo: %w", err)
	}
	path := filepath.Join(dir, comboFile)
	f, err := os.Create(path)
	if err != nil {
		return fmt.Errorf("persistence: create combo: %w", err)
	}
	gz := gzip.NewWriter(f)
	enc := gob.NewEncoder(gz)
	if err := enc.Encode(store); err != nil {
		_ = gz.Close()
		_ = f.Close()
		return fmt.Errorf("persistence: gob encode combo: %w", err)
	}
	if err := gz.Close(); err != nil {
		_ = f.Close()
		return fmt.Errorf("persistence: gzip close combo: %w", err)
	}
	return f.Close()
}

// MaxComboEntries returns the cap on matches per query. Exported so the
// manager can enforce the same limit in-memory before flushing.
func MaxComboEntries() int { return maxComboEntriesPerQuery }

// Now is overridable for tests; used when SaveCombo needs to timestamp.
var Now = func() time.Time { return time.Now() }
