// ---------------------------------------------------------------------------
// pages / PageView.tsx — EXIT wave 2 (W2-7, contracts Y1 + Y2 + Y3).
//
// THE renderer. A Y1 envelope goes in, a page comes out — and it knows the name
// of no page. That is the whole bet of the wave: wave 3 ships Collections and
// Procurement by adding a server-side builder and a registry key, not a
// component tree, and this file is where that promise is either kept or broken.
// If it ever grows `if (page.key === …)`, the envelope has stopped paying for
// itself and it belongs in the doc as an amendment, not in a branch here.
//
// TWO RULES ARE STRUCTURAL, NOT STYLISTIC:
//   · **Blocks render IN ORDER** (Y1 rule 7). The server decides the narrative;
//     the client does not re-sort it into what it thinks reads better.
//   · **An unknown block type is SKIPPED, never fatal.** That is the property
//     that lets the server learn a block type before this client does — a
//     deployment ordering the wave will actually hit.
// ---------------------------------------------------------------------------

import { DashHeader, DataTable, KpiRow, Section, ValidationPanel } from "../ui";
import type { DrillDescriptor, OnOpen } from "../ui";
import type {
  ChartBlock,
  DecompDescriptor,
  KpisBlock,
  PageBlock,
  PageControl,
  PageEnvelope,
  SectionBlock,
  TableBlock,
  ValidationBlock,
} from "../ui/types";
import { isRowDrillRule } from "../ui/types";
import { SeriesChart } from "../viz/SeriesChart";
import type { ChartSpec } from "../viz/chartData";
import type { Row } from "../viz/types";

/** A `section` block labels what FOLLOWS it, so the renderer pairs each one
 *  with the blocks up to the next section. Without this the note and the table
 *  it explains are two unrelated slabs with a gap between them. */
interface Group {
  head?: SectionBlock;
  body: PageBlock[];
}

function group(blocks: PageBlock[]): Group[] {
  const out: Group[] = [];
  let cur: Group = { body: [] };
  for (const b of blocks) {
    if (b.type === "section") {
      if (cur.head || cur.body.length) out.push(cur);
      cur = { head: b as SectionBlock, body: [] };
    } else {
      cur.body.push(b);
    }
  }
  if (cur.head || cur.body.length) out.push(cur);
  return out;
}

function ChartBlockView({ block, onOpen }: { block: ChartBlock; onOpen?: OnOpen }) {
  const rule = block.drill;
  const rowByX = new Map<string, DecompDescriptor>();
  if (isRowDrillRule(rule)) {
    const xKey = (block.spec as { x?: string }).x;
    for (const r of block.rows) {
      const d = r[rule.row_key];
      const x = xKey ? r[xKey] : undefined;
      if (x != null && d && typeof d === "object" && (d as DecompDescriptor).kind === "decomp") {
        rowByX.set(String(x), d as DecompDescriptor);
      }
    }
  }
  // The server-sent per-bucket delta (the printed YoY %). A null stays absent —
  // the partial period carries no label, by the server's own rule.
  const deltaByX = new Map<string, number>();
  if (block.delta_key) {
    const xKey = (block.spec as { x?: string }).x;
    for (const r of block.rows) {
      const x = xKey ? r[xKey] : undefined;
      const v = r[block.delta_key];
      if (x != null && typeof v === "number" && Number.isFinite(v)) deltaByX.set(String(x), v);
    }
  }
  const pickable = !!onOpen && rowByX.size > 0;
  return (
    <>
      <SeriesChart
        spec={block.spec as unknown as ChartSpec}
        series={block.series ?? []}
        fields={block.fields}
        rows={block.rows as unknown as Row[]}
        xOrder={block.x_order}
        deltaByKey={deltaByX.size ? deltaByX : undefined}
        pickable={pickable}
        onPick={(x) => {
          const d = rowByX.get(x);
          if (d && onOpen) onOpen(d);
        }}
      />
      {/* Y1 rule 6 — even a chart discloses its cap. A trend that quietly drew
          12 of 18 months is the same defect as a truncated table. */}
      {block.shown != null && block.total != null && block.total > block.shown ? (
        <p className="pg-shown is-capped">
          Showing {block.shown} of {block.total} periods
        </p>
      ) : null}
    </>
  );
}

function BlockView({ block, onOpen }: { block: PageBlock; onOpen?: OnOpen }) {
  switch (block.type) {
    case "kpis":
      return <KpiRow items={(block as KpisBlock).items} onOpen={onOpen} />;
    case "table": {
      const t = block as TableBlock;
      return (
        <DataTable
          columns={t.columns}
          rows={t.rows}
          drill={t.drill}
          shown={t.shown}
          total={t.total}
          empty={t.empty}
          download={t.download}
          onOpen={onOpen}
        />
      );
    }
    case "chart":
      return <ChartBlockView block={block as ChartBlock} onOpen={onOpen} />;
    case "validation":
      return <ValidationPanel checks={(block as ValidationBlock).checks} />;
    default:
      // ⛔ Y1 rule 7. Not an error, not a placeholder, not a console warning the
      // user cannot see — nothing. A block type this client does not know is a
      // server that is ahead of it, which is the normal state during a rollout.
      return null;
  }
}

export function PageView({
  page,
  onControl,
  onOpen,
  busy,
}: {
  page: PageEnvelope;
  onControl?: (key: string, value: string) => void;
  onOpen?: (d: DrillDescriptor) => void;
  busy?: boolean;
}) {
  const groups = group(page.blocks);
  return (
    <div className="pg-page">
      <DashHeader
        title={page.title}
        asOf={page.as_of}
        subtitle={page.subtitle}
        controls={page.controls as PageControl[] | undefined}
        onControl={onControl}
        busy={busy}
      />
      {groups.map((g, i) =>
        g.head ? (
          <Section key={g.head.key ?? `s${i}`} label={g.head.label} note={g.head.note}>
            {g.body.map((b, j) => (
              <BlockView key={b.key ?? `b${j}`} block={b} onOpen={onOpen} />
            ))}
          </Section>
        ) : (
          <div key={`g${i}`}>
            {g.body.map((b, j) => (
              <BlockView key={b.key ?? `b${j}`} block={b} onOpen={onOpen} />
            ))}
          </div>
        )
      )}
    </div>
  );
}
