package agent

import (
	"context"
	"encoding/json"
	"log/slog"
	"os"
	"path/filepath"
	"sort"
	"sync"
	"time"

	"github.com/jackc/pgx/v5/pgxpool"
)

// RecipeStore abstracts recipe persistence so the Agent works identically
// across webhook (PostgreSQL), CLI (file), and any future mode.
type RecipeStore interface {
	Load(ctx context.Context) []Recipe
	Save(ctx context.Context, recipe *Recipe)
}

const (
	loadLimit     = 30
	cacheTTL      = 10 * time.Minute
)

// ---- PostgreSQL-backed store (production) ----

type PostgresRecipeStore struct {
	pool      *pgxpool.Pool
	mu        sync.RWMutex
	cached    []Recipe
	cachedAt  time.Time
}

// NewPostgresRecipeStore creates a store backed by PostgreSQL.
// databaseURL is a connection string like "postgres://user:pass@host:5432/dbname".
// Auto-creates the recipes table on first use.
func NewPostgresRecipeStore(ctx context.Context, databaseURL string) (*PostgresRecipeStore, error) {
	pool, err := pgxpool.New(ctx, databaseURL)
	if err != nil {
		return nil, err
	}
	if err := pool.Ping(ctx); err != nil {
		pool.Close()
		return nil, err
	}

	s := &PostgresRecipeStore{pool: pool}
	if err := s.ensureTable(ctx); err != nil {
		pool.Close()
		return nil, err
	}

	slog.Info("recipe_store: postgres connected")
	return s, nil
}

func (s *PostgresRecipeStore) ensureTable(ctx context.Context) error {
	_, err := s.pool.Exec(ctx, `
		CREATE TABLE IF NOT EXISTS skills (
			task        VARCHAR(255) PRIMARY KEY,
			content     TEXT NOT NULL,
			hit_count   BIGINT NOT NULL DEFAULT 0,
			created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
			updated_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
			last_hit_at TIMESTAMPTZ
		)
	`)
	return err
}

func (s *PostgresRecipeStore) Close() {
	s.pool.Close()
}

func (s *PostgresRecipeStore) Load(ctx context.Context) []Recipe {
	s.mu.RLock()
	if s.cached != nil && time.Since(s.cachedAt) < cacheTTL {
		out := make([]Recipe, len(s.cached))
		copy(out, s.cached)
		s.mu.RUnlock()
		slog.Info("recipe_store: loaded (cache)", "count", len(out))
		// Increment hit_count asynchronously — don't block the caller.
		go func() {
			qctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
			defer cancel()
			_, _ = s.pool.Exec(qctx,
				`UPDATE skills SET hit_count = hit_count + 1, last_hit_at = NOW()
				 WHERE task IN (SELECT task FROM skills ORDER BY updated_at DESC LIMIT $1)`, loadLimit)
		}()
		return out
	}
	s.mu.RUnlock()

	rows, err := s.pool.Query(ctx,
		`UPDATE skills SET hit_count = hit_count + 1, last_hit_at = NOW()
		 WHERE task IN (SELECT task FROM skills ORDER BY updated_at DESC LIMIT $1)
		 RETURNING task, content, hit_count, created_at, updated_at, last_hit_at`, loadLimit)
	if err != nil {
		slog.Error("recipe_store: load failed", "error", err)
		return nil
	}
	defer rows.Close()

	var recipes []Recipe
	for rows.Next() {
		var r Recipe
		var createdAt, updatedAt time.Time
		var lastHitAt *time.Time
		if err := rows.Scan(&r.Task, &r.Content, &r.HitCount, &createdAt, &updatedAt, &lastHitAt); err != nil {
			continue
		}
		r.CreatedAt = createdAt.Format(time.RFC3339)
		r.UpdatedAt = updatedAt.Format(time.RFC3339)
		if lastHitAt != nil {
			r.LastHitAt = lastHitAt.Format(time.RFC3339)
		}
		recipes = append(recipes, r)
	}

	// Update cache.
	s.mu.Lock()
	s.cached = make([]Recipe, len(recipes))
	copy(s.cached, recipes)
	s.cachedAt = time.Now()
	s.mu.Unlock()

	if len(recipes) > 0 {
		slog.Info("recipe_store: loaded (postgres)", "count", len(recipes))
	}
	return recipes
}

func (s *PostgresRecipeStore) Save(ctx context.Context, recipe *Recipe) {
	if recipe == nil || recipe.Task == "" {
		return
	}

	_, err := s.pool.Exec(ctx, `
		INSERT INTO skills (task, content, created_at, updated_at)
		VALUES ($1, $2, NOW(), NOW())
		ON CONFLICT (task) DO UPDATE SET
			content    = EXCLUDED.content,
			updated_at = NOW()
	`, recipe.Task, recipe.Content)

	if err != nil {
		slog.Error("recipe_store: save failed", "error", err, "task", recipe.Task)
		return
	}

	// Invalidate cache so next Load picks up the new recipe.
	s.mu.Lock()
	s.cached = nil
	s.mu.Unlock()

	slog.Info("recipe_store: saved (postgres)", "task", recipe.Task)
}

// ---- File-backed store (CLI, local dev) ----

type FileRecipeStore struct {
	path string
	mu   sync.Mutex
}

// NewFileRecipeStore creates a store that persists recipes to a JSON file.
// If path is empty, defaults to .astra/recipes.json in the current working directory.
func NewFileRecipeStore(path string) *FileRecipeStore {
	if path == "" {
		path = filepath.Join(".astra", "recipes.json")
	}
	return &FileRecipeStore{path: path}
}

func (s *FileRecipeStore) Load(_ context.Context) []Recipe {
	s.mu.Lock()
	defer s.mu.Unlock()

	all := s.readFile()
	if len(all) == 0 {
		return nil
	}

	sort.Slice(all, func(i, j int) bool {
		return all[i].UpdatedAt > all[j].UpdatedAt
	})

	limit := loadLimit
	if limit > len(all) {
		limit = len(all)
	}

	now := time.Now().UTC().Format(time.RFC3339)
	for i := 0; i < limit; i++ {
		all[i].HitCount++
		all[i].LastHitAt = now
	}
	s.writeFile(all)
	return all[:limit]
}

func (s *FileRecipeStore) Save(_ context.Context, recipe *Recipe) {
	if recipe == nil || recipe.Task == "" {
		return
	}
	s.mu.Lock()
	defer s.mu.Unlock()

	recipes := s.readFile()

	found := false
	for i, r := range recipes {
		if r.Task == recipe.Task {
			recipe.HitCount = r.HitCount
			recipe.CreatedAt = r.CreatedAt
			recipes[i] = *recipe
			found = true
			break
		}
	}
	if !found {
		recipes = append(recipes, *recipe)
	}

	s.writeFile(recipes)
	slog.Info("recipe_store: saved (file)", "task", recipe.Task, "path", s.path, "total", len(recipes))
}

func (s *FileRecipeStore) readFile() []Recipe {
	b, err := os.ReadFile(s.path)
	if err != nil {
		return nil
	}
	var recipes []Recipe
	if err := json.Unmarshal(b, &recipes); err != nil {
		return nil
	}
	return recipes
}

func (s *FileRecipeStore) writeFile(recipes []Recipe) {
	dir := filepath.Dir(s.path)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		slog.Error("recipe_store: mkdir failed", "error", err, "dir", dir)
		return
	}
	b, err := json.MarshalIndent(recipes, "", "  ")
	if err != nil {
		return
	}
	if err := os.WriteFile(s.path, b, 0o644); err != nil {
		slog.Error("recipe_store: write failed", "error", err, "path", s.path)
	}
}

// ensure interface compliance
var (
	_ RecipeStore = (*PostgresRecipeStore)(nil)
	_ RecipeStore = (*FileRecipeStore)(nil)
)
