// ---------------------------------------------------------------------------
// query/QueryPage.tsx — WAVE 32 item 7 (ruling R1, contract C5): the Query module.
//
// Ask a question about ONE database you already have; the assistant answers with
// a VIEW, you read what it will actually do, and you keep it or you do not.
//
// ⛔ CHROME, NOT A REGISTRY MODULE, and `granted` is why that is allowed. `nav.ts`'s
// law for this set is one sentence — "A CHROME ROUTE RENDERS NOTHING THE SERVER DID
// NOT ALREADY GRANT" — so the database picker is the nav's OWN entries, handed down
// by the Shell, never a directory this page fetches. (A registry row would have been
// worse than wrong: every provisioned tenant's `modules` list is `['analyst',
// 'automation']`, so a registry key is silently OMITTED from the rail in every
// tenant while every gate stays green.)
//
// ⛔ NO SECOND GRID. Opening a saved query NAVIGATES to that database and asks the
// real surface to select the view (`VIEW_OPEN_EVENT`), which is how the view renders
// in its declared kind without this file learning what a kanban is. That channel's
// own contract says it: "the SHELL routes to the table and the GRID owns view
// selection, so neither has to learn the other's state."
//
// ⭐ AND THIS IS ITS FIRST EMITTER. `VIEW_OPEN_EVENT` was added in wave 20 for an
// alert's click-through and has had a LISTENER (`CustomerGrid`) and no sender ever
// since — the deep link it was built for was never wired ([[artifact-with-no-importer]]
// on the emit side). Two consequences handled here: the listener checks its own view
// list first (`views.some(...)`), so an event that arrives before the target grid has
// loaded is DROPPED, and there is no acknowledgement channel to wait on. So the emit
// is repeated on a short bounded schedule and `selectView` early-returns once the view
// is active, which makes a re-emit free rather than a flicker.
//
// ⚠ THE SENTENCE UNDER THE ANSWER IS THE LOAD-BEARING PART OF THIS PAGE. Measured in
// the prototype (W32-T50 section 4d): a free model produced a spec whose every column
// was real, every op legal, kind in the vocabulary and target granted — and which
// answered a different question ("Revenue by Creator" against a database holding no
// revenue). No server-side validation can see that class. So the server DERIVES a
// read-back from the spec it accepted, this page shows it, and nothing is saved until
// a person says so. Never shorten `explain` away to make the page tidier.
//
// ⚠ NO CLIENT UNION OVER `kind` — the server's vocabulary arrives as a string (the
// wave-9 law), so a kind we do not have a label for renders as itself rather than
// dropping the row.
// ---------------------------------------------------------------------------

import { useCallback, useEffect, useState } from "react";
import { VIEW_OPEN_EVENT, signal } from "../apiContract";
import { databaseEntries } from "../shell/nav";
import type { NavEntry } from "../shell/nav";
import { buildQuery, deleteQuery, fetchQueries, saveQuery } from "./queryApi";
import type { BuildResult, SavedQuery } from "./queryApi";
import "./query.css";

export interface QueryPageProps {
  /**
   * ⛔ REQUIRED, and it is the nav's own entries. An optional prop here would
   * degrade to "the feature does not exist" the first time a mount forgot it,
   * which is indistinguishable from never having been built (the PRD's words for
   * this wiring). The Shell already holds these — passing them is also what keeps
   * the chrome law true by construction rather than by promise.
   */
  granted: NavEntry[];
}

/** Human words for a kind. A kind absent from here renders as itself, never dropped. */
const KIND_LABEL: Record<string, string> = {
  grid: "Table",
  list: "List",
  chart: "Chart",
  kanban: "Board",
  calendar: "Calendar",
  timeseries: "Time series",
  map: "Map",
};

/**
 * When the view is opened in another surface, the target grid may not have loaded its
 * views yet — and its listener drops an event naming a view it cannot see. Re-emit on
 * this schedule (ms); `selectView` is idempotent, so a late arrival costs nothing.
 */
export const OPEN_RETRIES = [0, 250, 700, 1500, 2600] as const;

export function openSavedView(scope: string, viewId: string): void {
  if (typeof window === "undefined") return;
  if (window.location.hash.replace(/^#\/?/, "") !== scope) {
    window.location.hash = `#/${scope}`;
  }
  for (const delay of OPEN_RETRIES) {
    window.setTimeout(() => signal(VIEW_OPEN_EVENT, { topic: scope, viewId }), delay);
  }
}

/**
 * The answer block: a refusal, or the read-back plus the two ways out.
 *
 * ⚠ A NAMED EXPORT ON PURPOSE, so `verify_query`'s render leg can paint the three states
 * directly. `renderToString` does not run effects, so the states this page reaches only
 * AFTER a fetch would otherwise be unprovable without a browser — and the read-back and
 * the refusal sentence are precisely the parts that must not quietly stop rendering
 * ([[ui-invisible-to-assertions]]).
 */
export function QueryAnswer({ result, saving, onKeep, onDiscard }: {
  result: BuildResult;
  saving?: boolean;
  onKeep: () => void;
  onDiscard: () => void;
}) {
  if (result.refused) {
    return (
      <section className="qy-answer is-refused" role="status">
        <p className="qy-answer-head">That is not a view this database can draw</p>
        <p className="qy-answer-body">{result.refused}</p>
      </section>
    );
  }
  if (!result.spec) return null;
  return (
    <section className="qy-answer">
      <p className="qy-answer-head">Here is what that view would do</p>
      {/* ⛔ THE READ-BACK. Derived by the SERVER from the spec it accepted, so it
          describes what will be built rather than what the model meant. */}
      {result.explain ? <p className="qy-answer-body">{result.explain}</p> : null}
      <div className="qy-answer-acts">
        <button type="button" className="qy-btn qy-btn--primary" disabled={saving}
                onClick={onKeep}>
          {saving ? "Saving…" : "Keep this view"}
        </button>
        <button type="button" className="qy-btn" onClick={onDiscard}>
          Discard
        </button>
      </div>
    </section>
  );
}

/** One saved view. Named for the same reason as {@link QueryAnswer}. */
export function SavedQueryRow({ view, dbLabel, onOpen, onDelete }: {
  view: SavedQuery;
  dbLabel: string;
  onOpen: () => void;
  onDelete: () => void;
}) {
  return (
    <li className="qy-row">
      <div className="qy-row-main">
        <button type="button" className="qy-row-open" onClick={onOpen}>
          {view.name}
        </button>
        <span className="qy-row-meta">
          {KIND_LABEL[view.kind] ?? view.kind} · {dbLabel}
        </span>
      </div>
      {view.question ? <p className="qy-row-q">“{view.question}”</p> : null}
      {view.explain ? <p className="qy-row-explain">{view.explain}</p> : null}
      <div className="qy-row-acts">
        <button type="button" className="qy-btn" onClick={onOpen}>
          Open
        </button>
        <button type="button" className="qy-btn qy-btn--quiet" onClick={onDelete}>
          Delete
        </button>
      </div>
    </li>
  );
}

export default function QueryPage({ granted }: QueryPageProps) {
  const [scope, setScope] = useState<string>("");
  const [question, setQuestion] = useState("");
  const [busy, setBusy] = useState(false);
  const [result, setResult] = useState<BuildResult | null>(null);
  const [problem, setProblem] = useState("");
  const [saved, setSaved] = useState<SavedQuery[]>([]);
  const [builtins, setBuiltins] = useState<string[]>([]);
  const [saving, setSaving] = useState(false);
  const [loaded, setLoaded] = useState(false);

  /**
   * The databases this page may ASK ABOUT.
   *
   * `databaseEntries` drops SURFACE rows (that fact is decided where rows are built, not
   * re-derived here) and a group head has no destination. ⛔ BUT "granted and not a surface" is
   * WIDER THAN THE BUILD DOOR ACCEPTS: it takes any `ut_*` key plus the built-in topics it
   * publishes as `builtins`. Offering anything else would be a control that lies — the picker
   * would list a database and the build call would 404 on it. So the server's own accepted set is
   * mirrored, never guessed, and until it arrives only `ut_*` keys are offered (fail NARROW).
   */
  const databases = databaseEntries(granted).filter(
    (e) => e.kind !== "group" && (e.key.startsWith("ut_") || builtins.includes(e.key))
  );

  // Keep the picker on a database that still exists: a grant can be withdrawn while
  // this page is open, and the server prunes on read, so the client must not pin a key
  // the payload no longer carries.
  useEffect(() => {
    if (databases.length && !databases.some((d) => d.key === scope)) {
      setScope(databases[0].key);
    }
  }, [databases, scope]);

  const reload = useCallback(async () => {
    const res = await fetchQueries();
    setLoaded(true);
    if (res.ok) {
      setSaved(res.value.views);
      setBuiltins(res.value.builtins);
    }
  }, []);

  useEffect(() => {
    void reload();
  }, [reload]);

  const ask = useCallback(async () => {
    const q = question.trim();
    if (!q || !scope || busy) return;
    setBusy(true);
    setProblem("");
    setResult(null);
    const res = await buildQuery(q, scope);
    setBusy(false);
    if (!res.ok) {
      setProblem(res.message);
      return;
    }
    setResult(res.value);
  }, [busy, question, scope]);

  const keep = useCallback(async () => {
    if (!result?.spec || saving) return;
    setSaving(true);
    const res = await saveQuery(question.trim(), scope, result.spec);
    setSaving(false);
    if (!res.ok) {
      setProblem(res.message);
      return;
    }
    setResult(null);
    setQuestion("");
    await reload();
  }, [question, reload, result, saving, scope]);

  const forget = useCallback(async (id: string) => {
    const res = await deleteQuery(id);
    if (!res.ok) {
      setProblem(res.message);
      return;
    }
    await reload();
  }, [reload]);

  const labelOf = (key: string) =>
    databases.find((d) => d.key === key)?.label ?? key;

  return (
    <div className="qy-page">
      <h1 className="qy-title">Query</h1>
      <p className="qy-lede">
        Ask about one of your databases. You get a view you can read before you keep it —
        and a plain answer when the question is not one this product can draw.
      </p>

      {databases.length === 0 ? (
        // ⚠ TWO DIFFERENT FACTS, TWO DIFFERENT SENTENCES. Before the index arrives the offerable
        // set is deliberately narrow (see `databases`), so "you have no database" would be a claim
        // this page cannot yet make.
        <p className="qy-note">
          {loaded
            ? "You do not have a database to ask about yet. Create one from the Database menu, "
              + "then come back and ask about it."
            : "Loading your databases…"}
        </p>
      ) : (
        <section className="qy-ask">
          <label className="qy-field">
            <span className="qy-label">Database</span>
            <select
              className="qy-select"
              value={scope}
              onChange={(e) => setScope(e.currentTarget.value)}
            >
              {databases.map((d) => (
                <option key={d.key} value={d.key}>
                  {d.label}
                </option>
              ))}
            </select>
          </label>
          <label className="qy-field qy-field--grow">
            <span className="qy-label">Question</span>
            <input
              className="qy-input"
              value={question}
              placeholder="Everyone with more than 50,000 followers, biggest first"
              onChange={(e) => setQuestion(e.currentTarget.value)}
              onKeyDown={(e) => {
                if (e.key === "Enter") void ask();
              }}
            />
          </label>
          <button
            type="button"
            className="qy-btn qy-btn--primary"
            disabled={busy || !question.trim()}
            onClick={() => void ask()}
          >
            {busy ? "Working…" : "Build a view"}
          </button>
        </section>
      )}

      {problem ? (
        <p className="qy-note is-warn" role="status">
          {problem}
        </p>
      ) : null}

      {/* A REFUSAL IS AN ANSWER, and it gets the same weight as a success — a sentence,
          never an error page (R1's own words). Both states live in `QueryAnswer`. */}
      {result ? (
        <QueryAnswer
          result={result}
          saving={saving}
          onKeep={() => void keep()}
          onDiscard={() => setResult(null)}
        />
      ) : null}

      <h2 className="qy-sub">Your views</h2>
      {!loaded ? (
        <p className="qy-note">
          <span className="lp-spin" role="status" aria-label="Loading" />
        </p>
      ) : saved.length === 0 ? (
        <p className="qy-note">
          Nothing yet. A view you keep lands here, and in the database it was built from.
        </p>
      ) : (
        <ul className="qy-list">
          {saved.map((v) => (
            <SavedQueryRow
              key={v.id}
              view={v}
              dbLabel={labelOf(v.scope)}
              onOpen={() => openSavedView(v.scope, v.viewId)}
              onDelete={() => void forget(v.id)}
            />
          ))}
        </ul>
      )}
    </div>
  );
}
