// src/stats/format.ts
import type { LayerRow, GainSummary } from "./reporter.ts";
import { estimateDollarsSaved } from "../pricing/estimate.ts";
import { tokensToUsd } from "./dollars.ts";

export interface RenderArgs {
  rows: LayerRow[];
  withDollars: boolean;
  model: string;
  format?: "table" | "json";
}

export function renderGainTable(args: RenderArgs): string {
  if (args.format === "json") {
    return JSON.stringify({
      rows: args.rows.map((r) => ({
        ...r,
        ...(args.withDollars
          ? { dollarsSaved: estimateDollarsSaved({ model: args.model, savedInputTokens: r.savedTokens, savedOutputTokens: 0 }) }
          : {}),
      })),
    }, null, 2);
  }

  const lines: string[] = [];
  const pad = (s: string, n: number) => s.padEnd(n);
  const headerCols = ["layer", "events", "before", "after", "saved", "%saved"];
  if (args.withDollars) headerCols.push("$saved");
  const widths = [18, 8, 12, 12, 12, 8, 10];

  lines.push(headerCols.map((c, i) => pad(c, widths[i]!)).join(" "));
  lines.push("─".repeat(widths.slice(0, headerCols.length).reduce((a, n) => a + n + 1, 0)));

  let totalBefore = 0, totalAfter = 0, totalSaved = 0;
  for (const r of args.rows) {
    totalBefore += r.tokensBefore;
    totalAfter += r.tokensAfter;
    totalSaved += r.savedTokens;
    const row = [
      pad(r.layerId, widths[0]!),
      pad(String(r.events), widths[1]!),
      pad(String(r.tokensBefore), widths[2]!),
      pad(String(r.tokensAfter), widths[3]!),
      pad(String(r.savedTokens), widths[4]!),
      pad((r.pctSaved * 100).toFixed(1) + "%", widths[5]!),
    ];
    if (args.withDollars) {
      const d = estimateDollarsSaved({ model: args.model, savedInputTokens: r.savedTokens, savedOutputTokens: 0 });
      row.push(pad("$" + d.toFixed(2), widths[6]!));
    }
    lines.push(row.join(" "));
  }
  lines.push("─".repeat(widths.slice(0, headerCols.length).reduce((a, n) => a + n + 1, 0)));
  const totalPct = totalBefore > 0 ? (totalSaved / totalBefore) * 100 : 0;
  const totalRow = [
    pad("TOTAL", widths[0]!),
    pad("", widths[1]!),
    pad(String(totalBefore), widths[2]!),
    pad(String(totalAfter), widths[3]!),
    pad(String(totalSaved), widths[4]!),
    pad(totalPct.toFixed(1) + "%", widths[5]!),
  ];
  if (args.withDollars) {
    const d = estimateDollarsSaved({ model: args.model, savedInputTokens: totalSaved, savedOutputTokens: 0 });
    totalRow.push(pad("$" + d.toFixed(2), widths[6]!));
  }
  lines.push(totalRow.join(" "));
  return lines.join("\n");
}

function formatTokens(n: number): string {
  if (n >= 1_000_000_000) return (n / 1_000_000_000).toFixed(1) + "B";
  if (n >= 1_000_000)     return (n / 1_000_000).toFixed(1) + "M";
  if (n >= 1_000)         return (n / 1_000).toFixed(1) + "K";
  return String(n);
}
function formatDollars(n: number): string {
  if (n >= 1000) return "$" + n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  return "$" + n.toFixed(2);
}
function formatPct(n: number): string { return (n * 100).toFixed(1) + "%"; }
function formatCount(n: number): string { return n.toLocaleString("en-US"); }
function truncate(s: string, n: number): string {
  if (s.length <= n) return s;
  return s.slice(0, n - 1) + "…";
}
function basenameOf(p: string): string { const i = p.lastIndexOf("/"); return i < 0 ? p : p.slice(i + 1); }
function bar(pct: number, width = 24): string {
  const filled = Math.max(0, Math.min(width, Math.round(pct * width)));
  return "█".repeat(filled) + "░".repeat(width - filled);
}

function renderHeader(s: GainSummary): string {
  const t = s.totals, td = s.today;
  const lines: string[] = [];
  lines.push(`fewtok Token Savings (${s.window.label})`);
  lines.push("═".repeat(72));
  lines.push("");
  lines.push(`Saved:    ${formatTokens(t.savedTokens).padStart(7)} tokens   (${formatDollars(t.dollarsSaved)})   (${formatPct(t.pctSaved)} of raw input)`);
  lines.push(`Today:    ${formatTokens(td.savedTokens).padStart(7)} tokens   (${formatDollars(td.dollarsSaved)})   (${formatPct(td.pctSaved)} of today's raw input)`);
  lines.push("");
  const cache = t.cacheHitRatio == null ? "n/a" : formatPct(t.cacheHitRatio);
  lines.push(`Sessions: ${formatCount(t.sessions)}    Requests: ${formatCount(t.requests)}    Anthropic cache hit: ${cache}`);
  lines.push(`Efficiency: ${bar(t.pctSaved)} ${formatPct(t.pctSaved)}`);
  return lines.join("\n");
}

function renderLayerTable(s: GainSummary): string {
  const lines: string[] = [];
  lines.push("");
  lines.push("By Layer (internal telemetry — estimated bytes/4, not billed tokens)");
  lines.push("─".repeat(72));
  const header = ["layer".padEnd(18), "events".padEnd(8), "before".padEnd(11),
                  "after".padEnd(11), "saved".padEnd(11), "compression"];
  lines.push(header.join(" "));
  lines.push("─".repeat(72));
  let tb = 0, ta = 0, ts = 0, te = 0;
  for (const r of s.layers) {
    tb += r.tokensBefore; ta += r.tokensAfter; ts += r.savedTokens; te += r.events;
    lines.push([
      r.layerId.padEnd(18),
      String(r.events).padEnd(8),
      formatCount(r.tokensBefore).padEnd(11),
      formatCount(r.tokensAfter).padEnd(11),
      formatCount(r.savedTokens).padEnd(11),
      formatPct(r.pctSaved),
    ].join(" "));
  }
  lines.push("─".repeat(72));
  const totalPct = tb > 0 ? ts / tb : 0;
  lines.push([
    "TOTAL (estimate)".padEnd(18),
    String(te).padEnd(8),
    formatCount(tb).padEnd(11),
    formatCount(ta).padEnd(11),
    formatCount(ts).padEnd(11),
    formatPct(totalPct),
  ].join(" "));
  lines.push("");
  lines.push("note: layer numbers are per-event byte estimates for tuning.");
  lines.push("Header `Saved` is the authoritative bill delta from Anthropic `usage`.");
  return lines.join("\n");
}

function renderUsageBlock(s: GainSummary): string {
  const t = s.totals;
  const lines: string[] = [];
  lines.push("");
  lines.push("True Anthropic usage");
  lines.push("─".repeat(72));
  const fmt = (l: string, n: number, l2: string, n2: number) =>
    `${l.padEnd(20)} ${formatTokens(n).padStart(8)} tok        ${l2.padEnd(20)} ${formatTokens(n2).padStart(8)} tok`;
  lines.push(fmt("raw input:", t.rawInputTokens, "compressed input:", t.compressedInputTokens));
  lines.push(fmt("output:", t.rawOutputTokens, "cache creation:", t.cacheCreationInputTokens));
  lines.push(`${"cache hits:".padEnd(20)} ${formatTokens(t.cacheHitInputTokens).padStart(8)} tok        ${"$ saved:".padEnd(20)} ${formatDollars(t.dollarsSaved).padStart(8)}`);
  return lines.join("\n");
}

function renderSideBySide(s: GainSummary): string {
  const lines: string[] = [];
  lines.push("");
  const leftHeader = "Top projects".padEnd(36);
  const rightHeader = "Top read-cached files";
  lines.push(leftHeader + "   " + rightHeader);
  lines.push("─".repeat(36) + "   " + "─".repeat(36));
  const left: string[] = s.topProjects.map((p) =>
    `${truncate(p.project, 21).padEnd(21)} ${formatTokens(p.savedTokens).padStart(6)}  ${formatPct(p.sharePct).padStart(5)}`);
  const right: string[] = s.topReadFiles.map((f) =>
    `${truncate(basenameOf(f.path), 22).padEnd(22)} ${(formatCount(f.hits) + "×").padStart(6)}  ${formatTokens(f.tokens).padStart(6)}`);
  const rowCount = Math.max(left.length, right.length, 1);
  for (let i = 0; i < rowCount; i++) {
    const l = (left[i] ?? "").padEnd(36);
    const r = right[i] ?? "";
    lines.push(l + "   " + r);
  }
  return lines.join("\n");
}

function renderFootnotes(s: GainSummary): string {
  if (s.unknownModels.length === 0) return "";
  return "\n(pricing unknown for: " + s.unknownModels.join(", ") + ")";
}

function renderProviderTable(s: GainSummary): string {
  const rows = s.topProviders ?? [];
  if (rows.length <= 1) return "";
  const lines: string[] = ["\nBy provider:"];
  for (const r of rows) {
    const pct = (r.sharePct * 100).toFixed(0).padStart(3);
    const saved = String(r.savedTokens).padStart(10);
    const reqs = String(r.requests).padStart(6);
    lines.push(`  ${r.provider.padEnd(14)} ${saved} saved  ${reqs} req  ${pct}%`);
  }
  return lines.join("\n");
}

export function renderGainSummary(s: GainSummary, _opts: { width: number }): string {
  if (s.totals.requests === 0) return "no data — run a session through fewtok first";
  return [
    renderHeader(s),
    renderLayerTable(s),
    renderUsageBlock(s),
    renderSideBySide(s),
    renderProviderTable(s),
    renderFootnotes(s),
  ].filter((x) => x !== "").join("\n");
}

// ── buildHeader ────────────────────────────────────────────────────────────

export interface HeaderInput {
  rawInputTokens: number;
  compressedInputTokens: number;
  cacheHitInputTokens: number;
  cacheCreationInputTokens: number;
  model: string;
}

/**
 * Returns a compact multi-line header string showing:
 *   Wire savings: XX.X%   $N.NN saved
 *   Cache hit rate: XX.X%
 * Returns "" when rawInputTokens === 0 (no traffic).
 */
export function buildHeader(h: HeaderInput): string {
  if (h.rawInputTokens <= 0) return "";

  const wireSaved = h.rawInputTokens - h.compressedInputTokens;
  const wirePct = wireSaved / h.rawInputTokens;

  const totalCacheable = h.cacheHitInputTokens + h.cacheCreationInputTokens;
  const cacheHitPct = totalCacheable > 0 ? h.cacheHitInputTokens / totalCacheable : 0;

  // Dollar value of wire compression savings (input tokens not sent)
  const wireDollars = tokensToUsd(wireSaved, h.model, "input");
  // Dollar value of cache hits (full price minus cached price for hit tokens)
  const cacheDollars =
    tokensToUsd(h.cacheHitInputTokens, h.model, "input") -
    tokensToUsd(h.cacheHitInputTokens, h.model, "cachedInput");
  const totalDollars = wireDollars + cacheDollars;

  const pctStr = (n: number) => `${(n * 100).toFixed(1)}%`;
  const dollarStr = `$${totalDollars.toFixed(2)}`;

  return [
    `Wire savings:   ${pctStr(wirePct)}   ${dollarStr} saved`,
    `Cache hit rate: ${pctStr(cacheHitPct)}`,
  ].join("\n");
}
