// Package corpus is the Phase 11 tracker rule corpus. Operator YAML files
// under `config/trackers/` describe each tracker's unwritten rules in a
// machine-readable form; the corpus loads them, caches them in memory and
// in the `tracker_rules` table, and exposes a query API.
package corpus

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"time"

	"gopkg.in/yaml.v3"
)

// Entry is one tracker's complete corpus.
type Entry struct {
	SchemaVersion int                    `yaml:"schema_version" json:"schema_version"`
	TrackerID     string                 `yaml:"tracker_id" json:"tracker_id"`
	Name          string                 `yaml:"name" json:"name"`
	HomeURL       string                 `yaml:"home_url" json:"home_url,omitempty"`

	// The rich Appendix E shape lives in Rules as nested maps so we don't
	// have to mirror every sub-field in Go. The schema is documented in
	// PROJECT.md Appendix E; the loader validates required top-level keys.
	Rules map[string]any `yaml:",inline" json:"rules"`
}

// Store wraps the on-disk corpus + the in-memory cache + the
// `tracker_rules` SQLite table.
type Store struct {
	dir    string
	db     *sql.DB
	mu     sync.RWMutex
	cache  map[string]Entry
}

// NewStore constructs a store. The operator's directory is the YAML root.
func NewStore(dir string, db *sql.DB) *Store {
	return &Store{dir: dir, db: db, cache: map[string]Entry{}}
}

// Reload re-reads every *.yaml under dir. Returns the count of loaded
// trackers and any first error encountered.
func (s *Store) Reload(ctx context.Context) (int, error) {
	if s.dir == "" {
		return 0, nil
	}
	entries, err := os.ReadDir(s.dir)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return 0, nil
		}
		return 0, err
	}
	fresh := map[string]Entry{}
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		if filepath.Ext(e.Name()) != ".yaml" && filepath.Ext(e.Name()) != ".yml" {
			continue
		}
		full := filepath.Join(s.dir, e.Name())
		data, err := os.ReadFile(full)
		if err != nil {
			return 0, err
		}
		var ent Entry
		if err := yaml.Unmarshal(data, &ent); err != nil {
			return 0, fmt.Errorf("corpus %s: %w", e.Name(), err)
		}
		if ent.SchemaVersion != 0 && ent.SchemaVersion != 1 {
			return 0, fmt.Errorf("corpus %s: unsupported schema_version %d", e.Name(), ent.SchemaVersion)
		}
		if ent.TrackerID == "" {
			return 0, fmt.Errorf("corpus %s: tracker_id required", e.Name())
		}
		fresh[ent.TrackerID] = ent
	}
	s.mu.Lock()
	s.cache = fresh
	s.mu.Unlock()

	// Persist into tracker_rules. One row per top-level rule key.
	if s.db != nil {
		now := time.Now().Unix()
		tx, err := s.db.BeginTx(ctx, nil)
		if err != nil {
			return len(fresh), err
		}
		defer func() { _ = tx.Rollback() }()
		_, _ = tx.ExecContext(ctx, `DELETE FROM tracker_rules`)
		for _, ent := range fresh {
			for k, v := range ent.Rules {
				b, _ := json.Marshal(v)
				_, _ = tx.ExecContext(ctx, `
					INSERT INTO tracker_rules(tracker_id, rule_key, rule_value_json, source_file, loaded_at)
					VALUES (?, ?, ?, ?, ?)
					ON CONFLICT(tracker_id, rule_key) DO UPDATE SET
					  rule_value_json = excluded.rule_value_json,
					  source_file = excluded.source_file,
					  loaded_at = excluded.loaded_at
				`, ent.TrackerID, k, string(b), ent.TrackerID+".yaml", now)
			}
		}
		if err := tx.Commit(); err != nil {
			return len(fresh), err
		}
	}
	return len(fresh), nil
}

// Get returns the cached entry for a tracker.
func (s *Store) Get(trackerID string) (Entry, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	e, ok := s.cache[trackerID]
	return e, ok
}

// All returns every cached entry.
func (s *Store) All() []Entry {
	s.mu.RLock()
	defer s.mu.RUnlock()
	out := make([]Entry, 0, len(s.cache))
	for _, e := range s.cache {
		out = append(out, e)
	}
	return out
}

// SeedTimeRequirement returns the configured seed-time-required for a
// torrent of sizeBytes on trackerID, using the by_size ladder when
// present and falling back to default_hours otherwise. Returns 0 if the
// corpus has no entry for this tracker.
func (s *Store) SeedTimeRequirement(trackerID string, sizeBytes int64) time.Duration {
	e, ok := s.Get(trackerID)
	if !ok {
		return 0
	}
	str, ok := e.Rules["seed_time_requirements"].(map[string]any)
	if !ok {
		return 0
	}
	def, _ := str["default_hours"].(float64)
	// by_size: ordered list of {max_size_gb, hours}.
	if bs, ok := str["by_size"].([]any); ok {
		sizeGB := float64(sizeBytes) / (1 << 30)
		for _, item := range bs {
			m, _ := item.(map[string]any)
			if m == nil {
				continue
			}
			max, hasMax := m["max_size_gb"].(float64)
			hours, _ := m["hours"].(float64)
			if !hasMax {
				return time.Duration(hours) * time.Hour
			}
			if sizeGB <= max {
				return time.Duration(hours) * time.Hour
			}
		}
	}
	return time.Duration(def) * time.Hour
}

// Warning is one structured advisory emitted by the warning system.
type Warning struct {
	RuleKey     string `json:"rule_key"`
	TrackerID   string `json:"tracker_id"`
	Description string `json:"description"`
}

// CheckActionAgainstCorpus inspects a proposed action against the tracker
// rule corpus and returns advisory warnings. Phase 11 implements one rule
// directly — "would deleting this torrent now violate hit-and-run seed
// time?"; further rule types are operator-extensible by adding to the
// switch.
func (s *Store) CheckActionAgainstCorpus(ctx context.Context, action map[string]any) []Warning {
	out := []Warning{}
	kind, _ := action["kind"].(string)
	trackerID, _ := action["tracker_id"].(string)
	hashF := action["info_hash"]
	hash, _ := hashF.(string)
	switch kind {
	case "delete_torrent":
		if trackerID == "" || hash == "" || s.db == nil {
			return out
		}
		var sizeBytes int64
		var firstSeenAt sql.NullInt64
		_ = s.db.QueryRowContext(ctx, `
			SELECT t.size_bytes, t.first_seen_at FROM torrents t WHERE t.info_hash = ?
		`, hash).Scan(&sizeBytes, &firstSeenAt)
		req := s.SeedTimeRequirement(trackerID, sizeBytes)
		if req == 0 || !firstSeenAt.Valid {
			return out
		}
		accumulated := time.Since(time.Unix(firstSeenAt.Int64, 0))
		if accumulated < req {
			out = append(out, Warning{
				RuleKey:   "seed_time_requirements",
				TrackerID: trackerID,
				Description: fmt.Sprintf(
					"This torrent has been seeded %s; tracker %s requires %s. Deleting now would create a hit-and-run.",
					accumulated.Round(time.Minute), trackerID, req.Round(time.Hour),
				),
			})
		}
	}
	return out
}
