// src/stats/renderGain.ts
// Pure renderer for `fewtok gain` (Wave 1 — C, spec §3 / plan Task C).
// No side-effects; all input arrives via GainAggregate.
//
// NOTE: NorthStar, VolumeRow, ProjectRow are defined locally until Worker B
// canonicalises them in reporter.ts. Worker D will rewire the imports.

import { sparkbar } from "./sparkbar.ts";
import { tokensToUsd } from "./dollars.ts";
import type { LayerRow, ProviderRow, GainTotals } from "./reporter.ts";

// ── Local interface copies (Worker B canonicalises in reporter.ts) ─────────────

export interface NorthStar {
  tokensSaved: number;
  requests: number;
}

export interface VolumeRow {
  requests: number;
  cacheHitTokens: number;
  cacheHitPct: number;
  avgLatencyMs: number;
  p99LatencyMs: number;
}

export interface ProjectRow {
  project: string;
  requests: number;
  tokensSaved: number;
}

export interface GainAggregate {
  /** e.g. "all-time" | "7d" | "24h" */
  window: string;
  /** Model id for dollar conversion (may have provider prefix) */
  model: string;
  northStarAllTime: NorthStar;
  northStarToday: NorthStar;
  layers: LayerRow[];
  /** Denominator for the impact bar */
  layersTotalSaved: number;
  /** Empty array when no project breakdown available */
  projects: ProjectRow[];
  volume: VolumeRow;
  /** Per-provider breakdown; empty when provider column not populated. */
  providers?: ProviderRow[];
}

// ── Render options ─────────────────────────────────────────────────────────────

export interface RenderGainOpts {
  /** Force ANSI color on/off. Default: auto-detect via NO_COLOR / isTTY. */
  color?: boolean;
}

// ── Number formatting helpers ──────────────────────────────────────────────────

/** Compact token count: 6_100_000 → "6.1M", 152_000 → "152.0K". */
export function fmtTokens(n: number): string {
  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);
}

/** Dollar amount: 18.42 → "$18.42". */
export function fmtDollars(n: number): string {
  return `$${n.toFixed(2)}`;
}

/** Ratio to percent string: 0.324 → "32.4%". */
export function fmtPct(ratio: number): string {
  return `${(ratio * 100).toFixed(1)}%`;
}

function pad(s: string, width: number): string {
  if (s.length >= width) return s.slice(0, width);
  return s + " ".repeat(width - s.length);
}

function rpad(s: string, width: number): string {
  if (s.length >= width) return s.slice(0, width);
  return " ".repeat(width - s.length) + s;
}

function hline(width: number): string {
  return "─".repeat(width);
}

function stripAnsi(s: string): string {
  return s.replace(/\x1b\[[0-9;]*m/g, "");
}

// ── ANSI colour helpers ────────────────────────────────────────────────────────

function useColor(opts: RenderGainOpts): boolean {
  if (opts.color === true) return true;
  if (opts.color === false) return false;
  if (process.env["NO_COLOR"] != null) return false;
  return process.stdout.isTTY === true;
}

function bold(s: string, c: boolean): string {
  return c ? `\x1b[1m${s}\x1b[0m` : s;
}

function green(s: string, c: boolean): string {
  return c ? `\x1b[32m${s}\x1b[0m` : s;
}

function dim(s: string, c: boolean): string {
  return c ? `\x1b[2m${s}\x1b[0m` : s;
}

// ── Section renderers ──────────────────────────────────────────────────────────

/**
 * Two north-star boxes side-by-side (spec §3.1).
 *
 *  ┌──────────────────────────────────────┐  ┌──────────────────────────────────────┐
 *  │ $ saved                              │  │ Tokens saved                         │
 *  │  $18.42       today  $0.46           │  │  6.1M         today  152.0K          │
 *  └──────────────────────────────────────┘  └──────────────────────────────────────┘
 */
function renderNorthStarBoxes(
  agg: GainAggregate,
  width: number,
  color: boolean,
): string {
  const boxW = Math.floor((width - 2) / 2);
  const inner = boxW - 2; // chars between the two │

  const allTimeDollars = tokensToUsd(agg.northStarAllTime.tokensSaved, agg.model, "input");
  const todayDollars = tokensToUsd(agg.northStarToday.tokensSaved, agg.model, "input");

  const dolBig = bold(green(fmtDollars(allTimeDollars), color), color);
  const dolSub = dim(`today  ${fmtDollars(todayDollars)}`, color);
  const tokBig = bold(green(fmtTokens(agg.northStarAllTime.tokensSaved), color), color);
  const tokSub = dim(`today  ${fmtTokens(agg.northStarToday.tokensSaved)}`, color);

  function box(label: string, big: string, sub: string): string[] {
    const top = "┌" + "─".repeat(boxW - 2) + "┐";
    const lab = "│ " + pad(label, inner - 1) + "│";

    const bigP = stripAnsi(big);
    const subP = stripAnsi(sub);
    // Build content to exactly `inner` plain chars, then splice ANSI back in.
    // Layout: "  " + big + <gap> + sub + <tail padding>
    // We fix gap at ≥1 then pad the tail to hit inner exactly.
    const gap = Math.max(1, inner - 2 - bigP.length - subP.length);
    // tail = remaining space after big+gap+sub, clamped ≥0
    const usedPlain = 2 + bigP.length + gap + subP.length;
    const tail = Math.max(0, inner - usedPlain);
    const mid = "│  " + big + " ".repeat(gap) + sub + " ".repeat(tail) + "│";

    const bot = "└" + "─".repeat(boxW - 2) + "┘";
    return [top, lab, mid, bot];
  }

  const L = box("$ saved", dolBig, dolSub);
  const R = box("Tokens saved", tokBig, tokSub);
  return L.map((l, i) => l + "  " + R[i]).join("\n");
}

/**
 * By-layer table with sparkbar impact column (spec §3.2).
 *
 *  Layer            Saved       %saved   Impact
 *  read-cache       5.5M        90.2%   ████████████ 90.2%
 *  macro-codec      600.0K       9.8%   █              9.8%
 *  ──────────────────────────────────────────────────────────
 *  TOTAL (estimate) 6.1M       100.0%
 */
function renderLayerTable(
  layers: LayerRow[],
  totalSaved: number,
  width: number,
  color: boolean,
): string {
  const CL = 18; // layer col
  const CS = 10; // saved col
  const CP = 8;  // pct col
  const BW = 12; // bar width

  const header = pad("Layer", CL) + rpad("Saved", CS) + rpad("%saved", CP) + "  Impact";
  const divider = hline(Math.min(width, CL + CS + CP + 2 + BW + 8));

  const sorted = [...layers].sort((a, b) => b.savedTokens - a.savedTokens);
  const rows = sorted.map((row) => {
    const pct = totalSaved > 0 ? row.savedTokens / totalSaved : 0;
    const bar = sparkbar(pct, BW);
    const pctStr = fmtPct(pct);
    return (
      pad(row.layerId, CL) +
      rpad(fmtTokens(row.savedTokens), CS) +
      rpad(pctStr, CP) +
      "  " + green(bar, color) + " " + pctStr
    );
  });

  const total =
    pad("TOTAL (estimate)", CL) +
    rpad(fmtTokens(totalSaved), CS) +
    rpad(fmtPct(1), CP);

  return [header, divider, ...rows, divider, dim(total, color)].join("\n");
}

/**
 * By-project table (skipped when projects.length <= 1, spec §3.3).
 */
function renderProjects(
  projects: ProjectRow[],
  totalSaved: number,
  color: boolean,
): string {
  const CP = 28; // project col
  const CS = 10; // saved col
  const CR = 8;  // requests col

  const header = bold(
    pad("Top projects", CP) + rpad("Saved", CS) + rpad("Req", CR),
    color,
  );
  const rows = projects.slice(0, 5).map((p) => {
    const name = pad(p.project.replace(/^.*\//, ""), CP);
    const pct = totalSaved > 0 ? p.tokensSaved / totalSaved : 0;
    return name + rpad(fmtTokens(p.tokensSaved), CS) + rpad(String(p.requests), CR) + "  " + fmtPct(pct);
  });
  return [header, ...rows].join("\n");
}

/**
 * Volume strip (spec §3.4).
 */
function renderVolumeStrip(volume: VolumeRow, color: boolean): string {
  const numFmt = (n: number) => n.toLocaleString("en-US");
  const lines = [
    bold("Volume", color),
    `Requests:            ${numFmt(volume.requests)}`,
    `Anthropic cache hit: ${fmtPct(volume.cacheHitPct)}`,
    `Avg latency:         ${volume.avgLatencyMs.toFixed(0)} ms`,
    `p99 latency:         ${volume.p99LatencyMs.toFixed(0)} ms`,
  ];
  return lines.join("\n");
}

/**
 * Provider breakdown section (§3.5): shown when > 1 provider detected.
 */
function renderProviders(providers: ProviderRow[], color: boolean): string {
  if (!providers || providers.length <= 1) return "";
  const CP = 16;
  const CS = 12;
  const CR = 8;
  const header = bold(
    pad("Provider", CP) + rpad("Saved", CS) + rpad("Req", CR) + rpad("Share", 7),
    color,
  );
  const rows = providers.map((r) => {
    const pct = (r.sharePct * 100).toFixed(0);
    return pad(r.provider, CP) + rpad(fmtTokens(r.savedTokens), CS) + rpad(String(r.requests), CR) + `${pct}%`;
  });
  return [bold("By provider", color), header, ...rows].join("\n");
}

// ── Public API ─────────────────────────────────────────────────────────────────

/**
 * Render a full `fewtok gain` summary to a string (spec §3).
 */
export function renderGain(agg: GainAggregate, opts: RenderGainOpts = {}): string {
  const width = 80;
  const color = useColor(opts);
  const sections: string[] = [];

  // Header: "fewtok gain · <window> · today"
  sections.push(bold(`fewtok gain · ${agg.window} · today`, color));
  sections.push(hline(width));

  // §3.1 North-star boxes
  sections.push(renderNorthStarBoxes(agg, width, color));
  sections.push("");

  // §3.2 By-layer table
  sections.push(renderLayerTable(agg.layers, agg.layersTotalSaved, width, color));
  sections.push("");

  // §3.3 Projects (skip if ≤1)
  if (agg.projects.length > 1) {
    sections.push(renderProjects(agg.projects, agg.layersTotalSaved, color));
    sections.push("");
  }

  // §3.4 Volume
  sections.push(renderVolumeStrip(agg.volume, color));
  sections.push("");

  // §3.5 Providers (skip if ≤1)
  const providerBlock = renderProviders(agg.providers ?? [], color);
  if (providerBlock) sections.push(providerBlock);

  return sections.join("\n");
}

/**
 * Render a "no data yet" placeholder.
 */
export function renderEmpty(
  reason: "no-traffic" | "empty-dict",
  port?: number,
): string {
  const msgs: Record<string, string> = {
    "no-traffic": "No requests through the proxy yet.",
    "empty-dict": "Dictionary is empty — no macro aliases configured.",
  };
  const lines = [
    "fewtok gain",
    "",
    `  No data yet: ${msgs[reason] ?? reason}`,
  ];
  if (port != null) {
    lines.push(`  Start the proxy:  fewtok start --port ${port}`);
  }
  return lines.join("\n");
}

/**
 * Renders an honest gain summary with wire%, cache%, and dollar savings
 * shown on separate lines. Suitable for the `ft gain` header.
 *
 * Wire savings % = (rawInput - compressed) / rawInput * 100
 * Cache hit %    = cacheHit / rawInput * 100
 */
export interface GainSummaryInput {
  requests: number;
  rawInputTokens: number;
  compressedInputTokens: number;
  cacheHitInputTokens: number;
  cacheCreationInputTokens: number;
  rawOutputTokens: number;
  totalLatencyMs: number;
  wireSavedBytes: number;
  wireTotalBytes: number;
}

export function renderGainSummary(t: GainSummaryInput | GainTotals): string {
  const comp = t.compressedInputTokens;
  const cacheHit = t.cacheHitInputTokens;
  const cacheCreate = t.cacheCreationInputTokens;
  const wireSavedBytes = t.wireSavedBytes;
  const wireTotalBytes = t.wireTotalBytes;

  const wireSaved = wireTotalBytes > 0 ? (wireSavedBytes / wireTotalBytes) * 100 : 0;
  const apiTotal = comp + cacheHit + cacheCreate;
  const cachePct = apiTotal > 0 ? (cacheHit / apiTotal) * 100 : 0;

  const dollarsSaved =
    "dollarsSaved" in t && typeof t.dollarsSaved === "number"
      ? t.dollarsSaved
      : 0;

  const fmt1 = (n: number) => n.toFixed(1);
  const fmtN = (n: number) => n.toLocaleString("en-US");
  const fmtUsd = (n: number) => `$${n.toFixed(2)}`;

  return [
    `Wire savings:  ${fmt1(wireSaved)}%  (${fmtN(wireSavedBytes)} bytes compressed away)`,
    `Cache hit:     ${fmt1(cachePct)}%  (${fmtN(cacheHit)} tok from cache)`,
    `Total saved:   ${fmtUsd(dollarsSaved)}`,
  ].join("\n");
}
