/**
 * KHAELOR
 * File: prototypes/tui-spike/candidate-b/main.tsx
 * Description: Candidate B — Ink 7 control (TUI_DESIGN §15.1 candidate "Ink 7, strictly bounded"):
 *              settled content through <Static> (written once), live region as a bounded dynamic
 *              tree (streaming tail, status line, composer, status bar). Same shared demo model
 *              and instrumentation as candidate A.
 *
 * Author: Simon-Pierre Boucher
 * Contact: contact@spboucher.ai
 */

import fs from "node:fs";
import React, { useEffect, useReducer, useState } from "react";
import { Box, Static, Text, render, useApp, useInput } from "ink";
import { DemoModel, STATUS_BAR, TAIL_CAP_LINES } from "../shared/demo";
import { SpikeMetrics, patchStdout } from "../shared/metrics";

const metrics = new SpikeMetrics("b-ink7");

// temporary diagnostic: log every stdout write (time, length, composer presence)
const writeLog: string[] = [];
if (process.env.WRITE_LOG) {
  const t0 = Date.now();
  const orig = process.stdout.write.bind(process.stdout);
  (process.stdout as unknown as { write: unknown }).write = (chunk: unknown, ...rest: unknown[]) => {
    const s = typeof chunk === "string" ? chunk : Buffer.from(chunk as Uint8Array).toString("utf8");
    writeLog.push(
      `${Date.now() - t0} len=${s.length} composer=${s.includes("❯ ") ? "Y" : "n"} ${JSON.stringify(s.slice(0, 1200))}`,
    );
    return (orig as (c: unknown, ...r: unknown[]) => boolean)(chunk, ...rest);
  };
  process.on("exit", () => {
    fs.writeFileSync(process.env.WRITE_LOG!, writeLog.join("\n") + "\n");
  });
}
const unpatch = patchStdout(process.stdout, metrics, () =>
  process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80,
);
const model = new DemoModel();

// Record stdin arrival time before Ink's own parser sees the byte (listener order).
const pendingTs: bigint[] = [];
process.stdin.on("data", () => {
  pendingTs.push(process.hrtime.bigint());
  if (pendingTs.length > 128) pendingTs.shift();
});

interface SettledItem {
  id: number;
  text: string;
}

function App(): React.JSX.Element {
  const { exit } = useApp();
  const [settled, setSettled] = useState<SettledItem[]>([]);
  const [, force] = useReducer((x: number) => x + 1, 0);

  useEffect(() => {
    const onSettled = (lines: string[]) =>
      setSettled((s) => [...s, { id: s.length, text: lines.join("\n") }]);
    const onDirty = () => force();
    model.on("settled", onSettled);
    model.on("dirty", onDirty);
    const ticker = setInterval(() => {
      if (model.status !== null) force();
    }, 100);
    model.start();
    return () => {
      model.off("settled", onSettled);
      model.off("dirty", onDirty);
      clearInterval(ticker);
    };
  }, []);

  useInput((input, key) => {
    const ts = pendingTs.shift();
    if (key.escape) {
      exit();
      return;
    }
    if (key.return) {
      model.handleKey({ type: "enter" });
      return;
    }
    if (key.backspace || key.delete) {
      model.handleKey({ type: "backspace" });
      return;
    }
    if (input.length > 0) {
      // A lagging event loop can coalesce "\r" + following chars into one chunk that Ink
      // delivers as string input with key.return=false — treat embedded CR/LF as Enter,
      // exactly like candidate A's decodeKeys.
      for (const ch of input)
        model.handleKey(ch === "\r" || ch === "\n" ? { type: "enter" } : { type: "char", ch });
      // Space keystrokes are excluded: Ink/Yoga trims trailing whitespace at line ends,
      // which would make a space-terminated needle unmatchable (verified empirically).
      if (ts !== undefined && input.trim().length > 0)
        metrics.expectEcho(ts, ("❯ " + model.composer).trimEnd());
    }
  });

  const snap = model.snapshot();
  const tailLines = snap.tail.length > 0 ? snap.tail.split("\n").slice(-TAIL_CAP_LINES) : [];
  const elapsed =
    snap.status === null ? null : ((Date.now() - snap.status.since) / 1000).toFixed(1);

  return (
    <>
      <Static items={settled}>
        {(item) => (
          <Text key={item.id} wrap="wrap">
            {item.text}
          </Text>
        )}
      </Static>
      <Box flexDirection="column">
        {tailLines.length > 0 && (
          <>
            <Text wrap="wrap">{tailLines.join("\n")}</Text>
            <Text> </Text>
          </>
        )}
        {snap.status !== null && (
          <>
            <Text>{`● ${snap.status.text} · ${elapsed}s`}</Text>
            <Text> </Text>
          </>
        )}
        <Text wrap="truncate">{"❯ " + snap.composer}</Text>
        <Text dimColor wrap="truncate">
          {STATUS_BAR}
        </Text>
      </Box>
    </>
  );
}

process.stdout.on("resize", () => {
  metrics.resizes++;
});

const instance = render(<App />, { exitOnCtrlC: true, patchConsole: false });
metrics.startMem();

// headless fallback: self-terminate if the driver never sends Esc
const durationMs = Number(process.env.DURATION_MS ?? 0);
if (durationMs > 0) setTimeout(() => instance.unmount(), durationMs + 4000).unref();

await instance.waitUntilExit();
model.stop();
metrics.turns = model.turn;
metrics.settledBlocks = model.settledCount;
const file = metrics.save(process.env.METRICS_OUT);
unpatch();
process.stdout.write(`metrics written: ${file}\n`);
process.exit(0);
