package persistence

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

const (
	keywordFile             = "keyword.gob.gz"
	maxKeywords             = 4000
	maxKeywordEntriesPerKey = 10
)

// KeywordMatch records one (keyword -> symbol) association. HitCount
// is how many times the agent picked this symbol following a query
// that contained the keyword; LastUsed is a unix timestamp (seconds)
// for decay and reaping. Structurally identical to ComboMatch but
// kept as a distinct type so the two stores' schemas can diverge.
type KeywordMatch struct {
	SymbolID string
	HitCount uint32
	LastUsed int64
}

// KeywordAssoc holds all recorded matches for one query keyword
// within a single repo. Ordered most-hit-first after any record.
type KeywordAssoc struct {
	Keyword string
	Matches []KeywordMatch
}

// KeywordStore is the persisted per-keyword association index for one
// repo. Where ComboStore keys on the whole normalized query,
// KeywordStore keys on each surviving query token -- so a new task
// with overlapping keywords but different phrasing still inherits the
// associations its keywords earned. Separate file from combo and
// feedback so each subsystem's schema evolves independently.
type KeywordStore struct {
	Version  string
	RepoPath string
	Keywords []KeywordAssoc
}

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

// LoadKeyword reads the keyword store from disk. A missing file is
// not an error -- it yields an empty store.
func LoadKeyword(dir string) (*KeywordStore, error) {
	path := filepath.Join(dir, keywordFile)
	f, err := os.Open(path)
	if err != nil {
		if os.IsNotExist(err) {
			return &KeywordStore{}, nil
		}
		return nil, fmt.Errorf("persistence: open keyword: %w", err)
	}
	defer func() { _ = f.Close() }()

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

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

// SaveKeyword writes the keyword store with gob+gzip compression.
// Trims the oldest keywords if over cap so the file cannot grow
// unboundedly on a long-running daemon.
func SaveKeyword(dir string, store *KeywordStore) error {
	if len(store.Keywords) > maxKeywords {
		trim := len(store.Keywords) - maxKeywords
		store.Keywords = store.Keywords[trim:]
	}
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return fmt.Errorf("persistence: mkdir keyword: %w", err)
	}
	path := filepath.Join(dir, keywordFile)
	f, err := os.Create(path)
	if err != nil {
		return fmt.Errorf("persistence: create keyword: %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 keyword: %w", err)
	}
	if err := gz.Close(); err != nil {
		_ = f.Close()
		return fmt.Errorf("persistence: gzip close keyword: %w", err)
	}
	return f.Close()
}

// MaxKeywordEntries returns the cap on matches per keyword. Tighter
// than MaxComboEntries -- a single keyword is a coarser key than a
// whole query, so its match list is held shorter to stay precise.
func MaxKeywordEntries() int { return maxKeywordEntriesPerKey }
