// Package cli: cache.go implements the `orca cache` subcommand family
// (P00-T3, R-008) and the shared cache helpers used by the read-only
// list commands (node/job/ns list).
//
// The cache is optional: if the cache DB cannot be opened (missing dir,
// permissions, corrupt file) the list commands fall back to the
// uncached read path silently with a slog.Warn.
package cli

import (
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"time"

	"github.com/spf13/cobra"

	"git.cloudinit.dev/coreci/orca/internal/cache"
	"git.cloudinit.dev/coreci/orca/internal/paths"
)

// cacheHit reports whether the cache returned a fresh entry for
// (class, key). On any cache-open or read error it returns false (miss)
// and logs a warning — the caller proceeds to the uncached path. The
// cache never *creates* ORCA_HOME: if the parent directory is missing
// the cache is skipped silently so that source-read errors (e.g. `orca
// ns list` against a nonexistent ORCA_HOME) still surface.
func cacheHit(class, key string) ([]byte, bool) {
	if !cacheAvailable() {
		return nil, false
	}
	c, err := cache.Open(paths.CacheDB())
	if err != nil {
		slog.Warn("cache: open failed, falling back to uncached path", "class", class, "err", err)
		return nil, false
	}
	defer c.Close()
	val, _, err := c.Get(class, key)
	if err != nil {
		if !errors.Is(err, cache.ErrCacheMiss) {
			slog.Warn("cache: get failed, falling back to uncached path", "class", class, "err", err)
		}
		return nil, false
	}
	return val, true
}

// cachePopulate stores val for (class, key) with the given ttl. Errors
// are logged but never returned — a failed populate must not break
// the list command.
func cachePopulate(class, key string, val []byte, ttl time.Duration) {
	if !cacheAvailable() {
		return
	}
	c, err := cache.Open(paths.CacheDB())
	if err != nil {
		slog.Warn("cache: open failed during populate", "class", class, "err", err)
		return
	}
	defer c.Close()
	if err := c.Set(class, key, val, ttl); err != nil {
		slog.Warn("cache: populate failed", "class", class, "err", err)
	}
}

// cacheAvailable reports whether the cache DB parent dir (ORCA_HOME)
// exists. The cache layer must never create ORCA_HOME; doing so would
// mask source-read errors like `orca ns list` against a missing home.
func cacheAvailable() bool {
	info, err := os.Stat(paths.Root())
	if err != nil || !info.IsDir() {
		return false
	}
	return true
}

// cacheGetList returns the cached JSON list for (class, key), or nil
// if miss/any error. It is the read-side helper for list commands.
func cacheGetList(class, key string, out any) bool {
	val, ok := cacheHit(class, key)
	if !ok {
		return false
	}
	if err := json.Unmarshal(val, out); err != nil {
		slog.Warn("cache: unmarshal failed, falling back to uncached path", "class", class, "err", err)
		return false
	}
	return true
}

// cachePutList stores list as JSON under (class, key) with ttl. Used
// by list commands after fetching from source.
func cachePutList(class, key string, list any, ttl time.Duration) {
	val, err := json.Marshal(list)
	if err != nil {
		slog.Warn("cache: marshal failed during populate", "class", class, "err", err)
		return
	}
	cachePopulate(class, key, val, ttl)
}

// Per-class TTLs (P00-T2).
const (
	cacheNodeTTL       = 30 * time.Second
	cacheJobTTL        = 10 * time.Second
	cacheNamespaceTTL  = 60 * time.Second
	cacheNodeClass     = "nodes"
	cacheJobClass      = "jobs"
	cacheNamespaceClass = "namespaces"
	cacheListKey       = "list"
)

// --- `orca cache` CLI (P00-T3) ---

var cacheCmd = &cobra.Command{
	Use:   "cache",
	Short: "Inspect or invalidate the orca CLI cache",
	Long: `Manage the CLI-side SQLite cache (R-008) at
` + "`" + `ORCA_HOME/orca_cache.db` + "`" + `.

Subcommands:
  show           — print per-class entry counts, total size, oldest entry
  invalidate <c> — drop all entries for a class (e.g. "nodes", "jobs")
  invalidate-all — drop every entry in the cache

Read-only list commands (node/job/ns list) populate the cache; writes
bypass it. The --watch flag bypasses the cache entirely (streaming).`,
}

var cacheShowCmd = &cobra.Command{
	Use:   "show",
	Short: "Print cache stats (per-class counts, sizes, oldest entry)",
	Args:  cobra.NoArgs,
	RunE: func(cmd *cobra.Command, args []string) error {
		c, err := cache.Open(paths.CacheDB())
		if err != nil {
			return fmt.Errorf("open cache: %w", err)
		}
		defer c.Close()
		stats, err := c.Stats()
		if err != nil {
			return fmt.Errorf("cache stats: %w", err)
		}
		if jsonOutput {
			return printJSON(stats)
		}
		out := cmd.OutOrStdout()
		if len(stats) == 0 {
			fmt.Fprintln(out, "Cache is empty.")
			return nil
		}
		fmt.Fprintf(out, "%-20s  %-8s  %-12s  %s\n", "CLASS", "COUNT", "BYTES", "OLDEST")
		var totalCount, totalBytes int64
		for _, s := range stats {
			oldest := time.Unix(0, s.OldestAt).UTC().Format(time.RFC3339)
			if s.OldestAt == 0 {
				oldest = "-"
			}
			fmt.Fprintf(out, "%-20s  %-8d  %-12d  %s\n", s.Class, s.Count, s.Bytes, oldest)
			totalCount += int64(s.Count)
			totalBytes += s.Bytes
		}
		fmt.Fprintf(out, "%-20s  %-8d  %-12d\n", "TOTAL", totalCount, totalBytes)
		return nil
	},
}

var cacheInvalidateCmd = &cobra.Command{
	Use:   "invalidate <class>",
	Short: "Drop all entries for a cache class",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		class := args[0]
		c, err := cache.Open(paths.CacheDB())
		if err != nil {
			return fmt.Errorf("open cache: %w", err)
		}
		defer c.Close()
		if err := c.Invalidate(class); err != nil {
			return fmt.Errorf("invalidate %s: %w", class, err)
		}
		if jsonOutput {
			return printJSON(map[string]string{"class": class, "status": "invalidated"})
		}
		fmt.Fprintf(cmd.OutOrStdout(), "✓ Cache invalidated: %s\n", class)
		return nil
	},
}

var cacheInvalidateAllCmd = &cobra.Command{
	Use:   "invalidate-all",
	Short: "Drop every entry in the cache",
	Args:  cobra.NoArgs,
	RunE: func(cmd *cobra.Command, args []string) error {
		c, err := cache.Open(paths.CacheDB())
		if err != nil {
			return fmt.Errorf("open cache: %w", err)
		}
		defer c.Close()
		if err := c.InvalidateAll(); err != nil {
			return fmt.Errorf("invalidate-all: %w", err)
		}
		if jsonOutput {
			return printJSON(map[string]string{"status": "invalidated"})
		}
		fmt.Fprintln(cmd.OutOrStdout(), "✓ Cache cleared.")
		return nil
	},
}

func init() {
	cacheCmd.AddCommand(cacheShowCmd)
	cacheCmd.AddCommand(cacheInvalidateCmd)
	cacheCmd.AddCommand(cacheInvalidateAllCmd)
	rootCmd.AddCommand(cacheCmd)
}
