/**
 * # react/HonestChart — human candlesticks over the Frame `json` Rendering (ADR-0052 §3)
 *
 * A standard human candlestick chart whose ONLY input is the typed Frame `json` Rendering
 * ({@link ../frame/render-json.ts FrameJson}) — there is NO generic-OHLC entry point, by doctrine: a
 * chart that cannot prove it invented nothing defeats the honest-chart claim. Three honesty rules,
 * all enforced here on the reading side:
 *
 *   - **UNKNOWN renders as a GAP.** A tape bucket with any UNKNOWN of {open,high,low,close} draws NO
 *     candle — a dashed time-axis tick marks that the bucket exists but its price is unknown. Never an
 *     interpolated candle, never a zero, never a neighbour carried forward.
 *   - **MODEL Fields are BADGED with their receipt.** The kernel's predictor/regime claims are `MODEL`
 *     Fields; each renders a badge whose `<title>` exposes the receipt (source + model version +
 *     confidence) on inspection. Provenance is never hidden.
 *   - **Fail-closed on a degraded MODEL Field.** A claim whose `MODEL` Field arrives WITHOUT its
 *     receipt is REFUSED ({@link assertModelReceipt}) — the chart throws rather than draw an
 *     unattributable model value.
 *
 * Rendered as pure SVG (no chart library, no runtime dep beyond the optional `react` peer). PURE +
 * deterministic: the same {@link FrameJson} yields byte-identical markup (no wall clock, no RNG) —
 * `React hosts Renderings; it never produces them`.
 */

import { createElement, type ReactElement } from "react";

import type { FrameJson, MarketJson, TapeRowJson } from "../frame/render-json.ts";
import { assertModelReceipt, isUnknown, valueOr } from "./frame-fields.ts";

// ── Fixed, deterministic layout geometry ─────────────────────────────────────
const M = { top: 40, right: 16, bottom: 28, left: 48 } as const;
const UP = "#16a34a";
const DOWN = "#dc2626";

/** 2-decimal fixed string — stable SVG coordinates (no float noise across platforms/runs). */
function r2(n: number): string {
  return n.toFixed(2);
}

export interface HonestChartProps {
  /** The typed Frame `json` Rendering — the ONLY data source (no generic-OHLC props, ADR-0052 §3). */
  readonly frame: FrameJson;
  /** Presentation only (not data): the SVG canvas size. Defaults 640×320. */
  readonly width?: number;
  readonly height?: number;
  /** Presentation only: a class on the root `<svg>` for host styling. */
  readonly className?: string;
}

/** The finite price extent over a tape (UNKNOWN legs skipped, never zero-filled). */
function priceExtent(tape: readonly TapeRowJson[]): { min: number; max: number } | null {
  let min = Infinity;
  let max = -Infinity;
  for (const row of tape) {
    for (const leg of [row.open, row.high, row.low, row.close]) {
      const v = valueOr(leg);
      if (v === null) continue;
      if (v < min) min = v;
      if (v > max) max = v;
    }
  }
  return min === Infinity ? null : { min, max };
}

/** Draw the candle body + wick, or a GAP tick when any OHLC leg is UNKNOWN. */
function candleEl(
  row: TapeRowJson,
  i: number,
  cx: number,
  bodyW: number,
  y: (p: number) => number,
  baselineY: number,
): ReactElement {
  const o = valueOr(row.open);
  const h = valueOr(row.high);
  const l = valueOr(row.low);
  const c = valueOr(row.close);

  // Fail-closed honesty: an incomplete bucket is a GAP, never an interpolated/zeroed candle.
  if (o === null || h === null || l === null || c === null) {
    return createElement("line", {
      key: i,
      className: "kestrel-candle kestrel-candle--gap",
      x1: r2(cx),
      x2: r2(cx),
      y1: r2(baselineY),
      y2: r2(baselineY - 6),
      stroke: "currentColor",
      strokeOpacity: 0.4,
      strokeDasharray: "2 2",
      "data-clock": row.clock,
      "data-gap": "true",
    });
  }

  const up = c >= o;
  const color = up ? UP : DOWN;
  const bodyTop = y(Math.max(o, c));
  const bodyH = Math.max(Math.abs(y(o) - y(c)), 1);
  return createElement(
    "g",
    {
      key: i,
      className: `kestrel-candle kestrel-candle--${up ? "up" : "down"}`,
      "data-clock": row.clock,
    },
    createElement("line", {
      key: "wick",
      x1: r2(cx),
      x2: r2(cx),
      y1: r2(y(h)),
      y2: r2(y(l)),
      stroke: color,
      strokeWidth: 1,
    }),
    createElement("rect", {
      key: "body",
      x: r2(cx - bodyW / 2),
      y: r2(bodyTop),
      width: r2(bodyW),
      height: r2(bodyH),
      fill: color,
    }),
  );
}

/** The MODEL-claim badge strip — each badge's `<title>` exposes its receipt on inspection. Refuses a
 * claim whose MODEL Field lacks its receipt (fail-closed). */
function claimBadges(kernel: FrameJson["kernel"], width: number): ReactElement[] {
  const badges: ReactElement[] = [];
  const badgeW = 132;
  kernel.claims.forEach((claim, i) => {
    const receipt = assertModelReceipt(claim.field, `claim[${i}] ${claim.claimType}`);
    const x = M.left + i * (badgeW + 6);
    if (x + badgeW > width) return; // off-canvas badges are dropped from the strip, never overlapped
    const conf = receipt.confidence.toFixed(2);
    const receiptText = `${claim.claimType} = ${String(claim.field.value)} (MODEL ${receipt.source} ${receipt.modelVer} · conf ${conf})`;
    badges.push(
      createElement(
        "g",
        {
          key: i,
          className: "kestrel-model-badge",
          transform: `translate(${r2(x)}, 8)`,
          "data-claim-type": claim.claimType,
          "data-source": receipt.source,
          "data-model-ver": receipt.modelVer,
          "data-confidence": conf,
        },
        createElement("title", { key: "t" }, receiptText),
        createElement("rect", {
          key: "r",
          x: 0,
          y: 0,
          width: badgeW,
          height: 18,
          rx: 3,
          fill: "none",
          stroke: "currentColor",
          strokeOpacity: 0.5,
        }),
        createElement("circle", { key: "c", cx: 9, cy: 9, r: 3, fill: UP, fillOpacity: 0.8 }),
        createElement(
          "text",
          { key: "x", x: 18, y: 13, fontSize: 10, fill: "currentColor" },
          `${claim.claimType} · MODEL`,
        ),
      ),
    );
  });
  return badges;
}

/**
 * Render the honest candlestick chart for a Frame `json` Rendering. UNKNOWN buckets become gaps,
 * MODEL claims are badged with their receipt, a degraded MODEL Field is refused (fail-closed). Pure
 * SVG, deterministic markup.
 */
export function HonestChart(props: HonestChartProps): ReactElement {
  const { frame } = props;
  const width = props.width ?? 640;
  const height = props.height ?? 320;
  const market: MarketJson = frame.market;
  const tape = market.tape;

  const plotW = width - M.left - M.right;
  const plotH = height - M.top - M.bottom;
  const baselineY = height - M.bottom;
  const extent = priceExtent(tape);

  // Price → y. When the extent is a single price (or none), collapse to the plot's vertical midpoint
  // rather than divide by zero — the candles still draw, just flat.
  const span = extent && extent.max > extent.min ? extent.max - extent.min : 0;
  const y = (p: number): number =>
    span === 0 || extent === null ? M.top + plotH / 2 : M.top + ((extent.max - p) / span) * plotH;

  const n = tape.length;
  const slotW = n > 0 ? plotW / n : plotW;
  const bodyW = Math.min(slotW * 0.6, 14);

  const candles = tape.map((row, i) => candleEl(row, i, M.left + slotW * (i + 0.5), bodyW, y, baselineY));
  const badges = claimBadges(frame.kernel, width);

  const children: ReactElement[] = [
    // Plot baseline (time axis) — a frame for the candles, not a price.
    createElement("line", {
      key: "axis",
      className: "kestrel-chart-axis",
      x1: r2(M.left),
      x2: r2(width - M.right),
      y1: r2(baselineY),
      y2: r2(baselineY),
      stroke: "currentColor",
      strokeOpacity: 0.3,
    }),
  ];
  // Price extent labels (honest: the real max/min drawn, or nothing when the tape is all-UNKNOWN).
  if (extent !== null) {
    children.push(
      createElement(
        "text",
        { key: "pmax", x: 4, y: r2(M.top + 4), fontSize: 10, fill: "currentColor", fillOpacity: 0.7 },
        r2(extent.max),
      ),
      createElement(
        "text",
        { key: "pmin", x: 4, y: r2(baselineY), fontSize: 10, fill: "currentColor", fillOpacity: 0.7 },
        r2(extent.min),
      ),
    );
  }
  children.push(...badges, ...candles);

  return createElement(
    "svg",
    {
      className: props.className ?? "kestrel-honest-chart",
      xmlns: "http://www.w3.org/2000/svg",
      viewBox: `0 0 ${width} ${height}`,
      width,
      height,
      role: "img",
      "data-instrument": market.instrument,
      "aria-label": `Candlestick chart for ${market.instrument}`,
    },
    children,
  );
}
