/**
 * Geometry math for the cost center charts: pure functions, no React / no
 * JSX, easy to unit test (see test/usage-charts.test.ts). The two "last 30
 * days" charts (the daily Token bar's three-segment stack, the daily cost
 * line + area) share one coordinate system — canvas width, padding, the
 * x()/y() mapping, SVG paths, x-axis label indices. The stacked bar's
 * horizontal layout (fixed 25px bar width, spacing ≥ bar width) is computed
 * by tokenBarLayout, and per-segment geometry (including per-segment hit
 * bands) is produced by barSegments; there's also pie-slice geometry (each
 * Agent's call count), success-rate normalization, and hover-bubble
 * placement (pointer lower-right, flipping at the edges). See chart-svg.tsx for the render skeleton.
 *
 * **Canvas width = the container's measured pixel width (1 canvas unit = 1
 * CSS pixel)**: the SVG no longer stretches/scales via a fixed viewBox —
 * a scaled-down "bar width" would be a fake pixel discounted by the
 * container's width (640 units squeezed into a half-width cell becomes
 * ~495px, a 0.77 factor), while requirements like "at least 25px wide" must
 * land on **real display pixels**. So the canvas width is supplied by the caller after measuring the container.
 */

/** Canvas height and padding (carried over from the original TrendChart constants; width is now measured from the container, see the file header). */
export const CHART_H = 200;
export const PAD_L = 46;
export const PAD_R = 8;
export const PAD_T = 10;
export const PAD_B = 22;

/** The daily Token chart's three buckets (bottom-to-top stacking order is output → cacheWrite → cacheRead). */
export type TokenBucketKey = "cacheRead" | "cacheWrite" | "output";

/** A chart's coordinate system: canvas width w, data point count n, y-axis bounds, and x()/y() mapping "index / value" to canvas coordinates. */
export interface ChartGeom {
  n: number;
  min: number;
  max: number;
  /** Total canvas width (= viewBox width = CSS pixel width). */
  w: number;
  innerW: number;
  innerH: number;
  step: number;
  x: (i: number) => number;
  y: (v: number) => number;
}

/**
 * Build the zero-baseline coordinate system: x takes each cell's midpoint,
 * y runs top-to-bottom with max as the full height, and w is the canvas width
 * in pixels. Invalid or zero-height ranges map values to the baseline rather
 * than producing NaN / Infinity.
 */
export function makeGeom(n: number, max: number, w: number): ChartGeom {
  return makeRangeGeom(n, 0, max, w);
}

/**
 * Build a coordinate system with an explicit y-axis range. Score charts use
 * this to zoom into the observed values; zero-baseline usage charts keep
 * calling makeGeom above.
 */
export function makeRangeGeom(n: number, min: number, max: number, w: number): ChartGeom {
  const innerW = Math.max(0, w - PAD_L - PAD_R);
  const innerH = CHART_H - PAD_T - PAD_B;
  const step = n > 0 ? innerW / n : innerW;
  const range = max - min;
  return {
    n,
    min,
    max,
    w,
    innerW,
    innerH,
    step,
    x: (i) => PAD_L + step * i + step / 2,
    y: (v) => PAD_T + innerH * (1 - (range > 0 ? (v - min) / range : 0)),
  };
}

/** Line path: `M x0,y0 L x1,y1 …` (identical to the original TrendChart's cost line). */
export function linePath(geom: ChartGeom, values: number[]): string {
  return values.map((v, i) => `${i === 0 ? "M" : "L"}${geom.x(i)},${geom.y(v)}`).join(" ");
}

/** Area path: the line drops vertically to the baseline (y=0) at the end, then closes back along the baseline to the start; used by the cost line's fill layer. */
export function areaPath(geom: ChartGeom, values: number[]): string {
  const n = values.length;
  if (n === 0) return "";
  const baseY = geom.y(0);
  const parts: string[] = [];
  for (let i = 0; i < n; i++)
    parts.push(`${i === 0 ? "M" : "L"}${geom.x(i)},${geom.y(values[i]!)}`);
  parts.push(`L${geom.x(n - 1)},${baseY}`);
  parts.push(`L${geom.x(0)},${baseY}`);
  parts.push("Z");
  return parts.join(" ");
}

/** Sparse x-axis label indices: first, middle, last (labeling every point would blur together when cells are narrow and there are many points). */
export function sparseLabelIdx(n: number): number[] {
  if (n <= 0) return [];
  if (n === 1) return [0];
  if (n === 2) return [0, 1];
  return [0, Math.floor((n - 1) / 2), n - 1];
}

/** Horizontal space a single date label (`MM-DD`, fontSize 9) takes up: roughly 28px of text width plus breathing room. */
const LABEL_MIN_PX = 40;

/**
 * Adaptive x-axis label indices: label more of them when each cell is wide
 * enough (the stride = the number of cells needed to fit the next label).
 * The Token bar chart's cells are each ≥ 2×25px, so in practice every day
 * gets labeled; when cells are narrow it automatically skips a few cells between labels so they don't blur together.
 */
export function autoLabelIdx(n: number, step: number): number[] {
  if (n <= 0) return [];
  const stride = step > 0 ? Math.max(1, Math.ceil(LABEL_MIN_PX / step)) : n;
  const idx: number[] = [];
  for (let i = 0; i < n; i += stride) idx.push(i);
  return idx;
}

/** Request success rate: no requests (total=0) is treated as 1 (matches the old bar's convention, avoiding 0/0). */
export function successRate(completed: number, total: number): number {
  return total > 0 ? completed / total : 1;
}

// —— Hover bubble placement (shared by both daily charts) ——

/** Gap between the pointer and the bubble's near corner: close enough to read as attached, far enough that the bubble never sits under the pointer. */
export const BUBBLE_OFFSET = 12;

/** The window the bubble must stay inside, in canvas coordinates: the scroll container's currently visible region (left/right move with horizontal scroll; the top is always 0). */
export interface BubbleView {
  left: number;
  right: number;
  bottom: number;
}

/**
 * Hover bubble placement: the preferred spot is the pointer's lower-right
 * (pointer + BUBBLE_OFFSET on both axes). Near an edge it **flips** to the
 * pointer's other side (right edge → lower-left, bottom edge → upper-right,
 * corner → upper-left): flipping keeps the bubble out from under the
 * pointer, where pure clamping would slide it back over the hovered mark.
 * The final clamp only guards the degenerate case (a window narrower than
 * the bubble on both sides of the pointer): the bubble then covers the
 * pointer, and a window narrower than the bubble itself still clips on the
 * right — unreachable at real card widths; the clamp just keeps the failure graceful.
 */
export function bubblePosition(
  px: number,
  py: number,
  bubbleW: number,
  bubbleH: number,
  view: BubbleView,
): { left: number; top: number } {
  let left = px + BUBBLE_OFFSET;
  if (left + bubbleW > view.right) left = px - BUBBLE_OFFSET - bubbleW;
  let top = py + BUBBLE_OFFSET;
  if (top + bubbleH > view.bottom) top = py - BUBBLE_OFFSET - bubbleH;
  return {
    left: Math.max(view.left, Math.min(left, view.right - bubbleW)),
    top: Math.max(0, Math.min(top, view.bottom - bubbleH)),
  };
}

// —— Daily Token: bar + three-segment stack ——

/**
 * Bar width (**real CSS pixels**, since 1 canvas unit = 1 pixel): **a fixed
 * value, not a minimum** — it used to be implemented as "no less than 25px",
 * which made bars stretch to fill the container when there were few points
 * (3 daily points could balloon to ~180px), defeating the intent of "25px
 * bar width". Now it's always 25px: scroll when it doesn't fit, and give the extra space to bar spacing when it does.
 */
export const BAR_W = 25;

/**
 * Minimum height of the per-segment hover hit band (canvas units = pixels,
 * innerH=168): in real data, output is often under 1% of the day's total
 * (sub-pixel height), and if the hit area equaled the visual rectangle it
 * would be un-hoverable — highlighting down to "every segment" is this
 * chart's core requirement. Widening the bar (≥25px) doesn't help the
 * vertical dimension either: a sub-pixel value stays sub-pixel, so this
 * floor must be kept.
 * (The hit band's **width** is a separate matter: it spans the full cell horizontally, see TokenBarChart's hitLayer.)
 */
export const MIN_HIT_H = 8;

/** The Token bar chart's horizontal layout: bar width (always BAR_W), total canvas width, and whether the content overflows the container (needing horizontal scroll). */
export interface TokenBarLayout {
  /** Bar width (CSS pixels): always BAR_W. */
  barW: number;
  /** Total canvas width (CSS pixels): fills the container, or overflows it per "bar + equal spacing". */
  chartW: number;
  /** Canvas is wider than the container: the caller needs horizontal scrolling to see it all. */
  scroll: boolean;
}

/**
 * Token bar chart horizontal layout: **bar width is always BAR_W (25 real
 * pixels), bar spacing ≥ bar width**.
 * - Many points (n×2×25px doesn't fit): each cell is exactly 2× the bar
 *   width, and the canvas overflows the container in real pixels →
 *   the container scrolls horizontally (no scaling, no squeezing);
 * - Few points (fits): the canvas fills the container, and **all the extra
 *   space goes to bar spacing** — bars no longer stretch (the old
 *   implementation treated 25px as a floor, letting 3 daily points' bars
 *   balloon to ~180px), they just stand farther apart.
 */
export function tokenBarLayout(containerW: number, n: number): TokenBarLayout {
  const innerW = Math.max(0, containerW - PAD_L - PAD_R);
  const needed = 2 * BAR_W * n; // inner width needed to lay out n bars (bar + equal spacing)
  if (needed <= innerW) return { barW: BAR_W, chartW: containerW, scroll: false };
  return { barW: BAR_W, chartW: PAD_L + needed + PAD_R, scroll: true };
}

/** One segment within a bar: the visual rectangle is drawn strictly to value, the hit band is computed separately (small segments are raised to be hoverable). */
export interface BarSegment {
  key: TokenBucketKey;
  value: number;
  /** Visual rectangle: segments sit flush against each other, total height = the day's total (no visual floor, no inflating the bar's height). */
  y: number;
  h: number;
  /** Hit band: fills the whole bar bottom-to-top with no overlap, small segments raised to minHit. */
  hitY: number;
  hitH: number;
}

/**
 * Hit-band height allocation (water-filling): segments below minHit are
 * raised to minHit, the rest share the remaining space proportionally to
 * their visual height; when the whole bar is shorter than k*minHit it
 * degrades to an even split (nobody can squeeze anybody else out).
 * Guarantee: the segment heights sum to total (the hit band fills the whole bar with no overlap).
 */
function hitHeights(heights: number[], total: number, minHit: number): number[] {
  const k = heights.length;
  if (k === 0) return [];
  if (total <= k * minHit) return heights.map(() => total / k);
  const small = new Set<number>();
  // Each round adds at most one segment to small; total > k*minHit guarantees not every segment gets added (some segment must end up with > minHit).
  for (;;) {
    const rest = total - small.size * minHit;
    const bigSum = heights.reduce((s, h, i) => (small.has(i) ? s : s + h), 0);
    const scaled = (i: number) =>
      bigSum > 0 ? (heights[i]! / bigSum) * rest : rest / (k - small.size);
    const next = heights.findIndex((_, i) => !small.has(i) && scaled(i) < minHit);
    if (next < 0) return heights.map((_, i) => (small.has(i) ? minHit : scaled(i)));
    small.add(next);
  }
}

/** Stacking order: bottom-to-top output → cacheWrite → cacheRead (matches TOKEN_COLORS' shading, darkest at the bottom). */
const STACK_ORDER: readonly TokenBucketKey[] = ["output", "cacheWrite", "cacheRead"];

/**
 * A bar's three-segment stack: bottom-to-top output → cacheWrite →
 * cacheRead, a zero-value bucket produces no segment (not drawn, and shouldn't be hoverable). The visual rectangle is drawn strictly to value; see hitHeights for the hit band.
 */
export function barSegments(
  geom: ChartGeom,
  p: { cacheRead: number; cacheWrite: number; output: number },
): BarSegment[] {
  const stack = STACK_ORDER.map((key) => ({ key, value: p[key] })).filter((b) => b.value > 0);
  if (stack.length === 0) return [];

  // Visual rectangles: the top edge is taken from the cumulative value, so segments sit flush against each other.
  const rects: Array<{ y: number; h: number }> = [];
  let cum = 0;
  for (const b of stack) {
    const bottom = geom.y(cum);
    cum += b.value;
    const top = geom.y(cum);
    rects.push({ y: top, h: bottom - top });
  }

  const base = geom.y(0);
  const hits = hitHeights(
    rects.map((r) => r.h),
    base - geom.y(cum),
    MIN_HIT_H,
  );
  let hitBottom = base;
  return stack.map((b, i) => {
    const hitH = hits[i]!;
    const seg: BarSegment = {
      key: b.key,
      value: b.value,
      y: rects[i]!.y,
      h: rects[i]!.h,
      hitY: hitBottom - hitH,
      hitH,
    };
    hitBottom -= hitH;
    return seg;
  });
}

// —— Each Agent's call count: pie chart ——

const TAU = Math.PI * 2;
/** Path coordinates keep 2 decimal places: the path string stays short and readable, and is easy to assert on in unit tests. */
const rnd = (v: number): number => Math.round(v * 100) / 100;

/** Take a point in polar coordinates: angle is measured from 12 o'clock, clockwise-positive (SVG's y-axis points down). */
function polar(cx: number, cy: number, r: number, angle: number): [number, number] {
  const a = angle - Math.PI / 2;
  return [rnd(cx + r * Math.cos(a)), rnd(cy + r * Math.sin(a))];
}

/** A single pie slice. */
export interface PieSlice {
  /** Index within the passed-in values (the caller uses this to look up name and color). */
  index: number;
  value: number;
  /** Fraction of the total [0,1]. */
  frac: number;
  /** Start/end angle (radians, clockwise from 12 o'clock). */
  start: number;
  end: number;
  /** The slice's path. */
  path: string;
}

/**
 * Slice path: `M center L start A radius … end Z`; sweep=1 means clockwise,
 * large-arc=1 when spanning more than a semicircle.
 * At 100% the start and end points coincide and the A command degrades into
 * "draws nothing" — split into two semicircular arcs to get a full circle.
 */
function slicePath(cx: number, cy: number, r: number, start: number, end: number): string {
  if (end - start >= TAU - 1e-9) {
    const [tx, ty] = polar(cx, cy, r, 0);
    const [bx, by] = polar(cx, cy, r, Math.PI);
    return `M${tx},${ty} A${r},${r} 0 1 1 ${bx},${by} A${r},${r} 0 1 1 ${tx},${ty} Z`;
  }
  const [x0, y0] = polar(cx, cy, r, start);
  const [x1, y1] = polar(cx, cy, r, end);
  const large = end - start > Math.PI ? 1 : 0;
  return `M${rnd(cx)},${rnd(cy)} L${x0},${y0} A${r},${r} 0 ${large} 1 ${x1},${y1} Z`;
}

/**
 * Pie slices: laid out clockwise from 12 o'clock in the order passed in,
 * each slice's angle = that value's share of the total.
 * Non-positive values produce no slice (a 0-degree arc is a degenerate
 * path); when the total ≤ 0, returns empty (the caller falls back to an empty state).
 */
export function pieSlices(values: number[], cx: number, cy: number, r: number): PieSlice[] {
  const total = values.reduce((s, v) => s + Math.max(0, v), 0);
  if (total <= 0) return [];
  const slices: PieSlice[] = [];
  let start = 0;
  values.forEach((value, index) => {
    if (value <= 0) return;
    const frac = value / total;
    const end = start + frac * TAU;
    slices.push({ index, value, frac, start, end, path: slicePath(cx, cy, r, start, end) });
    start = end;
  });
  return slices;
}
