// ---------------------------------------------------------------------------
// customer-grid / ScriptViewPanel.tsx — owner item 6's renderer (W36-T04, contract C3).
//
// ⛔ ITS OWN MODULE, AND THAT IS A HARNESS FACT AS WELL AS A TIDINESS ONE. It began inside
// `viewModes.tsx`, where the other display modes live — and that module `require`s `./cells`,
// which imports glide-data-grid, whose CJS build cannot load under this repo's node harness. So
// putting it there made the whole component untestable: the ONE thing this surface must prove is
// that a hostile spec is rendered as text, and that claim can only be made by RENDERING. Here the
// import graph is `react` + `./scriptViews` and nothing else, so `gridUx.test.ts` renders it for
// real and asserts on the HTML a person would receive.
// ⚠ The stylesheet is imported by `CustomerGrid.tsx` (the mount) for the same reason: tsc
// preserves a side-effect CSS import into the emitted CommonJS and node dies on it.
// ---------------------------------------------------------------------------

import { memo, useEffect, useRef, useState } from "react";
import { barScale, renderableSpec, specKindName } from "./scriptViews";
import type { DrawableSpec, ScriptRun, ScriptView as ScriptViewRecord } from "./scriptViews";

// ═══════════════════════════════════════════════════════════════════════════════════════════
// ⭐⭐ W36-T04 (owner item 6 · rulings R3/R10 · contract C3) — THE CODE-SCRIPT VIEW
//
// Owner, verbatim: *"Add code script as an interface (database View) so a user can build whatever
// they want through the Agent chat interface. be able to create any dashboard they want. User
// should have the ability to see the code AND the dashboard output of course."* Both halves are
// on screen at once, which is the whole of that sentence.
//
// ⛔⛔ NO BRANCH OF THIS RENDERER EXECUTES A STRING, AND THAT IS THE TICKET'S OWN `done-when`.
// R10 runs the Python in a SERVER sandbox exactly so the browser never has to trust the result;
// a renderer that then evaluated one would hand back everything the sandbox contains. So there is
// no `dangerouslySetInnerHTML`, no `new Function`, no `eval`, no `href`/`src` taken from a spec,
// and no `style` built from spec data anywhere below. Every value reaching the DOM is a SCALAR
// that `renderableSpec` admitted, rendered as TEXT — the one exception being a bar's width, which
// is a number clamped to a percentage here and never a string from the wire.
//
// ⛔ AN UNKNOWN `kind` IS A SENTENCE, NOT A BLANK. `spec` is whatever the script emitted (E's C3
// note 1: there is no fixed vocabulary, deliberately), so a view whose author invented
// `kind: "sankey"` is a legitimate thing this build cannot draw. Saying so, by name, is the
// difference between "this build does not know that shape yet" and "your script is broken".
//
// ⚠ `ok: false` IS A 200. A script that timed out or divided by zero asked a well-formed question
// whose ANSWER is that no view was produced; it is rendered beside the code, never thrown, and
// never a toast that disappears while the reader is looking at the line that caused it.
// ═══════════════════════════════════════════════════════════════════════════════════════════

function SpecTable({ spec }: { spec: DrawableSpec }) {
  return (
    <table className="cg-script-table">
      <thead>
        <tr>{(spec.columns ?? []).map((c, i) => <th key={`${c}:${i}`} scope="col">{c}</th>)}</tr>
      </thead>
      <tbody>
        {(spec.rows ?? []).map((row, r) => (
          <tr key={r}>{row.map((cell, c) => <td key={c}>{cell}</td>)}</tr>
        ))}
      </tbody>
    </table>
  );
}

function SpecMetrics({ spec }: { spec: DrawableSpec }) {
  return (
    <div className="cg-script-metrics">
      {(spec.items ?? []).map((item, i) => (
        <div className="cg-script-metric" key={`${item.label}:${i}`}>
          <div className="cg-script-metric-label">{item.label}</div>
          <div className="cg-script-metric-value">{item.value}</div>
          {item.note ? <div className="cg-script-metric-note">{item.note}</div> : null}
        </div>
      ))}
    </div>
  );
}

function SpecBars({ spec }: { spec: DrawableSpec }) {
  const series = spec.series ?? [];
  const scale = barScale(series);
  return (
    <div className="cg-script-bars">
      {series.map((s, i) => (
        <div className="cg-script-bar" key={`${s.label}:${i}`}>
          <span>{s.label}</span>
          <span className="cg-script-bar-track">
            {/* ⚠ The ONE computed style on this surface, and it is arithmetic on a NUMBER the
                spec reader already validated as finite, clamped here. Never a string from the
                wire, which is what would make this an injection point. */}
            <span
              className="cg-script-bar-fill"
              style={{ width: `${Math.min(100, Math.round((Math.abs(s.value) / scale) * 100))}%` }}
            />
          </span>
          <span className="cg-script-bar-value">{s.display}</span>
        </div>
      ))}
    </div>
  );
}

/** The refusal, and the four limit fields PRINTED rather than re-worded (C3 note 2). */
function ScriptRefusal({ run }: { run: ScriptRun }) {
  return (
    <div className="cg-script-refusal" role="status">
      <h4>This script did not produce a view</h4>
      <p>{run.error ?? "The run ended without emitting anything."}</p>
      <dl>
        <dt>Reason</dt>
        <dd>{run.code}</dd>
        {run.limit ? <><dt>Subject</dt><dd>{run.limit.subject}</dd></> : null}
        {run.limit ? <><dt>Effect</dt><dd>{run.limit.effect}</dd></> : null}
        {run.limit ? <><dt>Cause</dt><dd>{run.limit.cause}</dd></> : null}
        {run.limit ? <><dt>What to do</dt><dd>{run.limit.recommendation}</dd></> : null}
      </dl>
    </div>
  );
}

/**
 * ⭐⭐ W37-T41 — THE SHAPE OF A SCRIPT, SHOWN AS A PLACEHOLDER RATHER THAN PREFILLED.
 *
 * Picking Custom View mints a view with an EMPTY source (verified: `script_sandbox.check_source("")`
 * accepts it, which is why "mint on pick" was safe to hand lane C in mailbox E-7). Without this, the
 * person who has just created one meets a blank box and a Run button, which is not an invitation.
 *
 * ⛔ A PLACEHOLDER, NEVER A PREFILLED `value`. Prefilling would make the view DIRTY the moment it
 * opened, arm Save on code nobody wrote, and put a version nobody asked for into a 40-deep history.
 * A placeholder vanishes on the first keystroke and is never stored.
 * ⚠ It is the two lines the sandbox's own fixture uses, so the example a person is shown is the
 * example the gate proves runs [[test-double-built-from-the-producer]].
 */
const SCRIPT_STARTER =
  "rows = scoped_table()\nemit({'kind': 'kpi', 'label': 'Records', 'value': len(rows)})";


export function ScriptOutput({ run }: { run: ScriptRun | null }) {
  if (run === null)
    return <p className="cg-script-empty">Run the script to see what it draws.</p>;
  const drawable = run.ok ? renderableSpec(run.spec) : null;
  const claimed = specKindName(run.spec);
  return (
    <>
      {!run.ok ? <ScriptRefusal run={run} /> : null}
      {run.ok && drawable === null ? (
        <div className="cg-script-refusal" role="status">
          <h4>This build cannot draw that shape yet</h4>
          <p>
            {claimed
              ? `The script emitted a view of kind "${claimed}", which this version does not
                 render. It knows table, metrics, bars and text.`
              : `The script emitted a view with no recognisable kind. This version renders
                 table, metrics, bars and text.`}
          </p>
        </div>
      ) : null}
      {drawable !== null ? (
        <>
          {drawable.title ? <h3 className="cg-script-title">{drawable.title}</h3> : null}
          {drawable.kind === "table" ? <SpecTable spec={drawable} /> : null}
          {drawable.kind === "metrics" ? <SpecMetrics spec={drawable} /> : null}
          {drawable.kind === "bars" ? <SpecBars spec={drawable} /> : null}
          {drawable.kind === "text" ? <p className="cg-script-text">{drawable.body}</p> : null}
        </>
      ) : null}
      {run.stdout ? (
        <pre className="cg-script-stdout">
          {run.stdout}
          {run.truncated ? "\n[output truncated]" : ""}
        </pre>
      ) : null}
    </>
  );
}

/**
 * The whole View: the source on the left, what it drew on the right.
 *
 * ⚠ A DRAFT RUN NEEDS NO SAVE (C3 note 3), which is what makes this usable: type, run, save when
 * it works. The Save button is separate and deliberately not automatic, because a PUT is a NEW
 * VERSION and an autosaving editor would fill the 40-deep history with keystrokes.
 */
export const ScriptView = memo(function ScriptView({
  view,
  running,
  run,
  saving,
  readOnly = false,
  loading = false,
  reverting = false,
  onRun,
  onSave,
  onRevert,
}: {
  view: ScriptViewRecord | null;
  running: boolean;
  run: ScriptRun | null;
  saving?: boolean;
  readOnly?: boolean;
  /** ⚠ The host knows whether a fetch is outstanding; the panel cannot tell that from a null view.
   *  Defaulted so no existing caller breaks, which is safe HERE because the default is the older,
   *  more pessimistic sentence rather than a silently disabled feature. */
  loading?: boolean;
  reverting?: boolean;
  onRun: (draft: string) => void;
  onSave?: (source: string) => void;
  /**
   * ⭐ W37-T46 / D-338. Optional for the SAME reason `onSave` is: a read-only reader gets the
   * editor without the controls, and a caller that cannot revert simply does not pass this. The
   * control is not rendered at all when it is absent, rather than rendered and refused
   * [[permitted-is-not-answerable]].
   */
  onRevert?: (version: number) => void;
}) {
  const [draft, setDraft] = useState(view?.source ?? "");
  const loadedId = useRef<string | null>(null);
  useEffect(() => {
    // Re-seed only when a DIFFERENT view is opened. Re-seeding on every `view` identity would
    // discard what somebody is typing the moment the list refreshes underneath them.
    if (!view || loadedId.current === view.id) return;
    loadedId.current = view.id;
    setDraft(view.source);
  }, [view]);
  const dirty = view !== null && draft !== view.source;
  // ⛔ W37-T41: "STILL LOADING" AND "COULD NOT BE OPENED" ARE DIFFERENT FACTS AND USED TO SHARE A
  // SENTENCE. With mint-on-pick (E-7) the normal path now passes through `view === null` for as
  // long as the fetch takes, so the old copy told every person who created a Custom View that it
  // was broken, for a moment, every time. A state that renames itself is exactly what cost this
  // product a QA pass one wave ago [[a-store-outage-renames-itself-at-every-layer]].
  if (view === null)
    return (
      <div className="cg-script">
        <p className="cg-script-empty cg-script-empty--pad">
          {loading === true ? "Opening this view." : "This script view could not be opened."}
        </p>
      </div>
    );
  return (
    <div className="cg-script">
      <div className="cg-script-body">
        <section className="cg-script-pane" aria-label="Script source">
          <div className="cg-script-pane-head">
            <span>Code</span>
            <span className="cg-script-meta">
              {`v${view.version}`}
              {/* ⚠ "v5" alone cannot tell a roll-back from a coincidence of matching code. This is
                  the sentence that makes the history read as what happened. */}
              {view.restoredFrom ? `, restored from v${view.restoredFrom}` : ""}
              {dirty ? ", unsaved changes" : ""}
            </span>
            <span className="cg-script-spacer" />
            <button
              type="button"
              className="cg-script-ghost"
              disabled={!dirty || saving === true || readOnly || onSave === undefined}
              onClick={() => onSave?.(draft)}
            >
              {saving === true ? "Saving" : "Save version"}
            </button>
            <button
              type="button"
              className="cg-script-run"
              disabled={running}
              onClick={() => onRun(draft)}
            >
              {running ? "Running" : "Run"}
            </button>
          </div>
          {/* ⭐⭐ W37-T41 — THE PLACEHOLDER IS THE CREATE FLOW'S WHOLE EMPTY STATE, and it is the
              half that made item 10 worth reopening. Picking Custom View mints a view with an
              EMPTY source (verified: `script_sandbox.check_source("")` accepts it, which is why
              "mint on pick" was safe to hand lane C in mailbox E-7). Without this the person who
              just created one meets a blank box and a Run button, which is not an invitation, it
              is a puzzle.
              ⛔ A PLACEHOLDER, NOT A PREFILLED `value`. Prefilling would make the view DIRTY the
              moment it opened, arm Save on code nobody wrote, and put a version nobody asked for
              into a 40-deep history. It vanishes on the first keystroke and is never saved. */}
          {/* ⭐⭐ W37-T46 / D-338 — THE CONTROL THAT WAS MISSING. The API already returned
              `history` newest-first and the header already showed `v<n>`; there was simply no way
              to go back, which is the whole of D-338. It matters more now that a chat writes the
              code (R2): generation without roll-back means one bad answer costs somebody their
              view, and the person cannot tell in advance which answer that will be.
              ⛔ THE STRIP IS ABSENT, NOT DISABLED, WHEN THERE IS NOTHING TO GO BACK TO. A first
              version has no history, and a row of dead controls reads as a broken feature rather
              than as an empty one.
              ⚠ `trimmed` is PRINTED when it is non-zero. A history capped at 40 that silently
              showed 40 would let a reader conclude the view was only ever saved 40 times; saying
              how many were dropped is the difference between a partial record and a wrong one. */}
          {(view.history?.length ?? 0) > 0 ? (
            <div className="cg-script-history" aria-label="Earlier versions">
              <span className="cg-script-history-lead">Earlier</span>
              {[...(view.history ?? [])].map((h) => (
                <button
                  type="button"
                  key={h.version}
                  className="cg-script-history-item"
                  disabled={readOnly || reverting || onRevert === undefined}
                  title={`Go back to version ${h.version}, saved by ${h.author || "somebody"}`}
                  onClick={() => onRevert?.(h.version)}
                >
                  {`v${h.version}`}
                </button>
              ))}
              {view.trimmed > 0 ? (
                <span className="cg-script-history-trimmed">
                  {`${view.trimmed} older ${view.trimmed === 1 ? "version" : "versions"} dropped`}
                </span>
              ) : null}
              {reverting ? <span className="cg-script-history-trimmed">Going back</span> : null}
            </div>
          ) : null}
          <textarea
            className="cg-script-code"
            value={draft}
            spellCheck={false}
            readOnly={readOnly}
            placeholder={SCRIPT_STARTER}
            aria-label="Script source"
            onChange={(e) => setDraft(e.target.value)}
          />
        </section>
        <section className="cg-script-pane" aria-label="Script output">
          <div className="cg-script-pane-head">
            <span>Output</span>
            <span className="cg-script-spacer" />
            {run !== null ? (
              <span className="cg-script-meta">
                {run.ms > 0 ? `${run.ms} ms` : ""}
              </span>
            ) : null}
          </div>
          <div className="cg-script-out">
            <ScriptOutput run={run} />
          </div>
        </section>
      </div>
    </div>
  );
});
