package main

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/spf13/cobra"

	"github.com/zzet/gortex/internal/savings"
)

var (
	savingsModel    string
	savingsJSON     bool
	savingsReset    bool
	savingsCacheDir string
	savingsVerbose  bool
	savingsBarCells int
	savingsUTC      bool
)

var savingsCmd = &cobra.Command{
	Use:   "savings",
	Short: "Show token-savings dashboard (Today / Last 7 days / All time) + cost avoided",
	Long: `Renders a bar-chart dashboard of token-savings buckets persisted across
sessions: Today, Last 7 days, and All time. Each bucket shows a 16-cell
saved/total bar, percentage saved, raw token counts, and the USD value of
the tokens avoided (priced against popular models).

Savings accumulate every time a source-reading MCP tool (get_symbol_source,
batch_symbols, smart_context) returns a symbol or compressed view instead of
a full-file read. Cumulative totals live at ~/.cache/gortex/savings.json and
per-call events at the sibling ~/.cache/gortex/savings.jsonl — Today / 7-day
buckets come from the JSONL log, All time from the cumulative file.

Override the cache dir with --cache-dir, override pricing by exporting
GORTEX_MODEL_PRICING_JSON, and pass --verbose for a per-tool breakdown
inside each bucket.`,
	RunE: runSavings,
}

func init() {
	savingsCmd.Flags().StringVar(&savingsModel, "model", "", "highlight one model in USD output (default: show all)")
	savingsCmd.Flags().BoolVar(&savingsJSON, "json", false, "emit machine-readable JSON instead of the dashboard")
	savingsCmd.Flags().BoolVar(&savingsReset, "reset", false, "wipe cumulative totals + event log and exit")
	savingsCmd.Flags().StringVar(&savingsCacheDir, "cache-dir", "", "override graph cache directory (savings.json + savings.jsonl live here)")
	savingsCmd.Flags().BoolVarP(&savingsVerbose, "verbose", "v", false, "include per-tool breakdown for each bucket")
	savingsCmd.Flags().IntVar(&savingsBarCells, "bar-width", 16, "number of cells in each bar (default 16, matching semble)")
	savingsCmd.Flags().BoolVar(&savingsUTC, "utc", false, "bucket Today by UTC calendar (default: local time)")
	rootCmd.AddCommand(savingsCmd)
}

func runSavings(_ *cobra.Command, _ []string) error {
	path := savings.DefaultPath()
	if savingsCacheDir != "" {
		path = filepath.Join(savingsCacheDir, "savings.json")
	}
	eventsPath := savings.EventsPathFor(path)

	store, err := savings.Open(path)
	if err != nil {
		return fmt.Errorf("open savings store: %w", err)
	}

	if savingsReset {
		if err := store.Reset(); err != nil {
			return fmt.Errorf("reset: %w", err)
		}
		fmt.Fprintf(os.Stderr, "[gortex savings] reset cumulative totals at %s\n", path)
		if eventsPath != "" {
			fmt.Fprintf(os.Stderr, "[gortex savings] removed event log at %s\n", eventsPath)
		}
		return nil
	}

	snap := store.Snapshot()

	loc := time.Local
	if savingsUTC {
		loc = time.UTC
	}
	buckets, err := savings.BuildDashboard(eventsPath, snap.Totals, time.Now(), loc)
	if err != nil {
		// Don't fail the whole command on event-log read errors — fall
		// back to a dashboard with empty Today/7d buckets.
		fmt.Fprintf(os.Stderr, "[gortex savings] event log read failed: %v\n", err)
		buckets = []savings.Bucket{
			{Label: "Today"},
			{Label: "Last 7 days"},
			{Label: "All time", Totals: snap.Totals},
		}
	}

	if savingsJSON {
		return emitSavingsJSON(snap, buckets, path, eventsPath)
	}
	emitSavingsDashboard(snap, buckets, path, eventsPath)
	return nil
}

func emitSavingsJSON(snap savings.File, buckets []savings.Bucket, path, eventsPath string) error {
	bucketJSON := make([]map[string]any, 0, len(buckets))
	for _, b := range buckets {
		entry := map[string]any{
			"label":            b.Label,
			"tokens_saved":     b.Totals.TokensSaved,
			"tokens_returned":  b.Totals.TokensReturned,
			"calls_counted":    b.Totals.CallsCounted,
			"percent_saved":    savings.SavingsPercent(b.Totals),
			"cost_avoided_usd": savings.CostAvoidedAll(b.Totals.TokensSaved),
		}
		if len(b.PerTool) > 0 {
			tools := make([]map[string]any, 0, len(b.PerTool))
			for _, t := range b.PerTool {
				tools = append(tools, map[string]any{
					"tool":            t.Tool,
					"tokens_saved":    t.TokensSaved,
					"tokens_returned": t.TokensReturned,
					"calls_counted":   t.CallsCounted,
				})
			}
			entry["per_tool"] = tools
		}
		bucketJSON = append(bucketJSON, entry)
	}

	out := map[string]any{
		"path":             path,
		"events_path":      eventsPath,
		"first_seen":       snap.FirstSeen.Format(time.RFC3339),
		"last_updated":     snap.LastUpdated.Format(time.RFC3339),
		"tokens_saved":     snap.Totals.TokensSaved,
		"tokens_returned":  snap.Totals.TokensReturned,
		"calls_counted":    snap.Totals.CallsCounted,
		"cost_avoided_usd": savings.CostAvoidedAll(snap.Totals.TokensSaved),
		"buckets":          bucketJSON,
	}
	if len(snap.PerRepo) > 0 {
		out["per_repo"] = snap.PerRepo
	}
	if len(snap.PerLanguage) > 0 {
		out["per_language"] = snap.PerLanguage
	}
	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	return enc.Encode(out)
}

// emitSavingsDashboard renders the bar-chart dashboard. Layout mirrors
// semble's format_savings_report(): a header with cumulative USD on top,
// then one row per bucket with bar / percentage / token counts / dollar
// amount, optionally followed by a per-tool breakdown in --verbose mode.
func emitSavingsDashboard(snap savings.File, buckets []savings.Bucket, path, eventsPath string) {
	fmt.Println("Gortex Token Savings")
	fmt.Println("====================")
	fmt.Printf("Store:          %s\n", path)
	if eventsPath != "" {
		fmt.Printf("Event log:      %s\n", eventsPath)
	}
	if !snap.FirstSeen.IsZero() {
		fmt.Printf("Tracking since: %s\n", snap.FirstSeen.Format("2006-01-02 15:04"))
	}
	if !snap.LastUpdated.IsZero() {
		fmt.Printf("Last updated:   %s\n", snap.LastUpdated.Format("2006-01-02 15:04"))
	}

	// USD header — total avoided, headlined by the model we want to
	// feature (or the most-expensive one when --model is unset).
	costs := savings.CostAvoidedAll(snap.Totals.TokensSaved)
	headline, headlineModel := pickHeadlineCost(costs, savingsModel)
	fmt.Println()
	if snap.Totals.CallsCounted == 0 {
		fmt.Println("No source-reading tool calls recorded yet.")
		fmt.Println("Run `gortex mcp` and use get_symbol_source / batch_symbols / smart_context.")
		return
	}
	fmt.Printf("Cost avoided:   %s (%s) across %s calls · %s tokens saved\n",
		formatUSD(headline), headlineModel,
		humanInt(snap.Totals.CallsCounted), humanInt(snap.Totals.TokensSaved))

	// Per-bucket bar rows.
	fmt.Println()
	labelWidth := 0
	for _, b := range buckets {
		if l := len(b.Label); l > labelWidth {
			labelWidth = l
		}
	}
	for _, b := range buckets {
		renderBucketRow(b, labelWidth, headlineModel)
	}

	// USD-per-model table — same data the original CLI printed, kept
	// because it lets agents see opus vs sonnet vs haiku side-by-side.
	fmt.Println()
	fmt.Println("Cost avoided per model (all time):")
	if savingsModel != "" {
		amount := savings.CostAvoided(snap.Totals.TokensSaved, savingsModel)
		fmt.Printf("  %-20s %s\n", savingsModel, formatUSD(amount))
	} else {
		names := make([]string, 0, len(costs))
		for n := range costs {
			names = append(names, n)
		}
		sort.Strings(names)
		for _, n := range names {
			fmt.Printf("  %-20s %s\n", n, formatUSD(costs[n]))
		}
	}

	// --verbose: per-tool breakdown inside each bucket. Hidden by
	// default because the headline 3-row dashboard is what the user
	// asked for; this is the deep-dive.
	if savingsVerbose {
		for _, b := range buckets {
			if len(b.PerTool) == 0 {
				continue
			}
			fmt.Println()
			fmt.Printf("%s by tool:\n", b.Label)
			toolWidth := 0
			for _, t := range b.PerTool {
				if l := len(t.Tool); l > toolWidth {
					toolWidth = l
				}
			}
			for _, t := range b.PerTool {
				pct := savings.SavingsPercent(t.Totals)
				bar := savings.BarString(pct, savingsBarCells)
				fmt.Printf("  %-*s %s  %5.1f%%  saved %s (%d calls)\n",
					toolWidth, t.Tool, bar, pct,
					humanInt(t.TokensSaved), t.CallsCounted)
			}
		}
	}

	// Per-repo and per-language rollups (carried over from the original
	// dashboard — agents still find these useful at the bottom).
	printBucket("Per-repo totals (all time)", snap.PerRepo)
	printBucket("Per-language totals (all time)", snap.PerLanguage)
}

func renderBucketRow(b savings.Bucket, labelWidth int, headlineModel string) {
	pct := savings.SavingsPercent(b.Totals)
	bar := savings.BarString(pct, savingsBarCells)
	cost := savings.CostAvoided(b.Totals.TokensSaved, headlineModel)
	costStr := formatUSD(cost)
	if cost == 0 && b.Totals.TokensSaved == 0 {
		costStr = "  $0.00"
	}
	fmt.Printf("%-*s %s  %5.1f%%  saved %s / %s tokens  %s\n",
		labelWidth, b.Label, bar, pct,
		humanInt(b.Totals.TokensSaved),
		humanInt(b.Totals.TokensSaved+b.Totals.TokensReturned),
		costStr,
	)
}

// pickHeadlineCost selects which model's cost is featured at the top of
// the dashboard. Honors --model when set, otherwise picks the highest
// dollar amount so the headline is the most-impressive credible figure.
// Returns the cost and the model name actually used.
func pickHeadlineCost(costs map[string]float64, preferred string) (float64, string) {
	if preferred != "" {
		// Use the user's pick even when the model isn't in our table —
		// CostAvoided handles fuzzy matching and returns 0 for misses.
		return costs[preferred], preferred
	}
	if len(costs) == 0 {
		return 0, "n/a"
	}
	var bestName string
	var bestVal float64
	// Stable selection: highest value, then lexicographic name.
	names := make([]string, 0, len(costs))
	for n := range costs {
		names = append(names, n)
	}
	sort.Strings(names)
	for _, n := range names {
		if costs[n] > bestVal {
			bestVal = costs[n]
			bestName = n
		}
	}
	if bestName == "" {
		// All zeros — return any deterministic name so the header is
		// still meaningful ("$0.00 (claude-opus-4)").
		bestName = names[0]
	}
	return bestVal, bestName
}

// formatUSD picks a precision that's useful at the bucket scale: full
// cents for amounts >= $1, four decimals for the long-tail small ones so
// fresh installs don't show "$0.00" everywhere.
func formatUSD(usd float64) string {
	if usd >= 1.0 {
		return fmt.Sprintf("$%.2f", usd)
	}
	return fmt.Sprintf("$%.4f", usd)
}

// printBucket renders a sorted breakdown of name → Totals. Skipped when
// the bucket is empty so older savings files (with no per_language data)
// don't produce a noisy "Per-language totals: (none)" line.
func printBucket(title string, bucket map[string]*savings.Totals) {
	if len(bucket) == 0 {
		return
	}
	fmt.Println()
	fmt.Println(title + ":")
	keys := make([]string, 0, len(bucket))
	for k := range bucket {
		keys = append(keys, k)
	}
	sort.Slice(keys, func(i, j int) bool {
		// Heaviest first — agents care about where the savings come from.
		if a, b := bucket[keys[i]].TokensSaved, bucket[keys[j]].TokensSaved; a != b {
			return a > b
		}
		return keys[i] < keys[j]
	})
	for _, k := range keys {
		t := bucket[k]
		fmt.Printf("  %-24s tokens_saved=%-12s calls=%d\n",
			k, humanInt(t.TokensSaved), t.CallsCounted)
	}
}

// humanInt renders a number with thousands separators so big totals are readable.
func humanInt(n int64) string {
	if n < 0 {
		return "-" + humanInt(-n)
	}
	s := fmt.Sprintf("%d", n)
	if len(s) <= 3 {
		return s
	}
	// Insert commas every 3 digits from the right.
	var sb strings.Builder
	sb.Grow(len(s) + len(s)/3)
	prefix := len(s) % 3
	if prefix > 0 {
		sb.WriteString(s[:prefix])
		if len(s) > prefix {
			sb.WriteByte(',')
		}
	}
	for i := prefix; i < len(s); i += 3 {
		sb.WriteString(s[i : i+3])
		if i+3 < len(s) {
			sb.WriteByte(',')
		}
	}
	return sb.String()
}
