import { useCallback, useEffect, useMemo, useState } from "react";

import type { QueryOpenDetail } from "../apiContract";
import { QUERY_OPEN_EVENT } from "../apiContract";
import CustomerGrid from "../customer-grid/CustomerGrid";
import { OverlayProvider } from "../customer-grid/OverlaySurface";
import { gridScopeFor } from "../shell/dbFrame";
import { databaseEntries } from "../shell/nav";
import type { NavEntry } from "../shell/nav";
import { bindingFor, deleteQuery, fetchQueries, QUERY_BINDING_EVENT } from "./queryApi";
import type { QueryCitation, SavedQuery } from "./queryApi";
import { QueryEmpty, queryGroups, QueryRail } from "./queryParts";
import type { QueryGroup } from "./queryParts";
import "./query.css";

export interface QueryPageProps {
  granted: NavEntry[];
  /**
   * ⭐ R14 — the merged Assistant surface hosts the view list in its OWN left panel, so Query
   * renders no rail here. It is a flag rather than a second component because the SELECTION
   * contract does not change: the host emits `QUERY_OPEN_EVENT` through `openBuiltView`, which is
   * the same public qid hand-off an external link uses, and the listener below answers both.
   */
  hostedRail?: boolean;
  /** Bumped by the host after it deletes a view, so the two lists cannot drift apart. */
  refreshToken?: number;
  /**
   * ⛔ THE HOST'S SELECTION, AND IT IS A GUARANTEE THE EVENT CANNOT GIVE. `QUERY_OPEN_EVENT` stays
   * the published qid contract and still answers external callers, but it is a RACE: `retryEmit`'s
   * default ladder tops out at 2,600 ms, tuned for a click into a grid that is usually warm, while
   * the hosted workspace is a LAZY chunk over a cold grid that CLAUDE.md measures painting at
   * ~12 s. Every dispatch lands before the listener exists, none is acked, and the rail then
   * highlights a view the main column is not showing, 200 OK and silent. A prop arrives in the
   * same commit as the mount.
   */
  selectedId?: string;
}

/**
 * Query is a database frame over a virtual artefact.  Its public contract for B is the
 * `QUERY_BINDING_EVENT`: `dataScope` reads the one source database and `workspaceBinding` names
 * the opaque Query artefact.  The two must never be collapsed into a source-native workspace.
 *
 * ⭐⭐ OWNER ITEM 3 (2026-08-15) — THE RAIL IS THE NAVIGATION. See `queryParts.QueryRail`: the
 * top-right dropdown is gone AND replaced, rather than gone and left as an implicit `views[0]`.
 */
export default function QueryPage({ granted, hostedRail, refreshToken, selectedId }: QueryPageProps) {
  const [views, setViews] = useState<SavedQuery[]>([]);
  const [citations, setCitations] = useState<QueryCitation[]>([]);
  const [activeId, setActiveId] = useState("");
  const [loaded, setLoaded] = useState(false);
  const entries = databaseEntries(granted);
  const entryOf = (key: string) => entries.find((entry) => entry.key === key);
  const known = views.filter((view) => entryOf(view.source.database));
  const active = known.find((view) => view.id === activeId) ?? known[0] ?? null;

  const groups = useMemo<QueryGroup[]>(() => queryGroups(views, entries), [views, entries]);

  const reload = useCallback(async () => {
    const result = await fetchQueries();
    setLoaded(true);
    if (result.ok) {
      setViews(result.value.views);
      setCitations(result.value.citations);
    }
  }, []);

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

  /** The host owns the rail in the merged surface; a delete there must not leave this list stale. */
  useEffect(() => { if (refreshToken) void reload(); }, [refreshToken, reload]);

  /**
   * Set BEFORE the fetch lands, deliberately: `known` is empty until then, so the fallback below
   * does not fire, and by the time it can the id it would replace is already the right one.
   */
  useEffect(() => { if (selectedId) setActiveId(selectedId); }, [selectedId]);

  useEffect(() => {
    if (known.length && !known.some((view) => view.id === activeId)) setActiveId(known[0].id);
  }, [known, activeId]);

  /**
   * ⛔⛔ THE BINDING GOES DOWN AS A PROP, AND THAT IS NOT A TIDY-UP.
   * `CustomerGrid` registers its `QUERY_BINDING_EVENT` listener only when `isQueryPreviewRoute()`
   * is true, i.e. only while the hash starts `#/query`. R14 moves this surface INSIDE
   * `#/assistant`, where that predicate is false — so on the event path alone the grid would
   * receive no binding, `isQueryPreview` would be false, `previewReadOnly` with it, and the
   * merged surface would render a fully EDITABLE database grid over an immutable AI artefact,
   * 200 OK, with the immutability contract intact on paper. Every WRITE guard in the grid keys on
   * `queryBinding`; only the listener keys on the route. A prop is gated on neither.
   *
   * ⚠ Memoised because `CustomerGrid`'s preview effect lists `queryBinding` in its deps: a fresh
   * object per render re-enters it on every unrelated state change on this page.
   */
  const binding = useMemo(
    () => (active ? bindingFor(active, citations) : undefined), [active, citations]);

  useEffect(() => {
    if (!binding) return;
    // Still published for B, which is what `QUERY_BINDING_EVENT` is: one dispatch, not a ladder.
    // The 21 s retry ladder that used to be here existed for ONE failure — the grid mounting
    // after the event was fired — and a prop delivered in the same commit cannot have it.
    window.dispatchEvent(new CustomEvent(QUERY_BINDING_EVENT, { detail: binding }));
  }, [binding]);

  useEffect(() => {
    const onOpen = (event: Event) => {
      const qid = (event as CustomEvent<QueryOpenDetail>).detail?.qid;
      if (!qid) return;
      // ⛔ The assistant can hand over an artefact this page has not fetched yet — it was created
      // seconds ago, in the OTHER surface. Selecting it optimistically and reloading is what makes
      // "ask, then click the preview" work on a first visit; without the reload the id is unknown,
      // the guard drops it, and the click silently does nothing [[wrong-parent-not-broken-control]].
      setActiveId(qid);
      if (!views.some((view) => view.id === qid)) void reload();
    };
    window.addEventListener(QUERY_OPEN_EVENT, onOpen);
    return () => window.removeEventListener(QUERY_OPEN_EVENT, onOpen);
  }, [views, reload]);

  /** A renamed or duplicated artefact, merged in place: the rail owns the call, the page the list. */
  const upsert = useCallback((view: SavedQuery) => {
    setViews((current) => [view, ...current.filter((row) => row.id !== view.id)]);
  }, []);

  const remove = useCallback(async (qid: string) => {
    const result = await deleteQuery(qid);
    if (!result.ok) return;
    setViews((current) => current.filter((view) => view.id !== qid));
    setActiveId((current) => (current === qid ? "" : current));
  }, []);

  if (!active) return <QueryEmpty loaded={loaded} unnamed={views.length - known.length} />;
  /* ⛔ `const entry = entryOf(active.source.database)` STOOD HERE AND WENT WITH THE HEADER
     (W36-T67). Its ONLY reader was the deleted header's icon prop; `entryOf` itself survives
     because `known` above filters the view list with it. A lookup kept alive for a deleted
     consumer is
     the residue an unmount leaves when only the JSX is deleted, and tsc named it in one line. */
  return (
    <div className="shell-db-frame">
      {/*
        ⭐⭐ WAVE 36 · W36-T67 (owner item 7) — THE UNIVERSAL DATABASE HEADER IS UNMOUNTED HERE,
        AND ON THIS SURFACE IT WAS ALREADY A SECOND COPY OF SOMETHING VISIBLE.
        ⚠ THE COMPONENT IS NOT NAMED ANYWHERE IN THIS FILE ANY MORE, DELIBERATELY. F's W36-T51
        deletes it and finds its last references by searching; a comment carrying the symbol is
        a hit that costs somebody a read, and this very file already warns about prose becoming
        its own marker one block below [[prose-that-becomes-its-own-marker]].

        ⛔ ITEM 7'S OWN REASONING DOES NOT TRANSFER TO A QUERY, which is why this needed its own
        ticket rather than following the database one. Owner: the header is *"a waste of white
        space. User should be able to tell what database they are in just by seeing the first
        Unique ID field."* A Query has no Unique ID column of its own to lean on.
        ⭐ WHAT IT LEANS ON INSTEAD IS ALREADY ON SCREEN, and it is richer than the band that went:
        `queryGroups` groups the rail BY SOURCE DATABASE and heads each group with that database's
        label AND its icon — owner item 3 of 2026-08-15, *"each View correspond to the relevant
        database"*. So `label={active.source.label}` was the same fact, painted twice, one of them
        costing a full-width band. The `hostedRail` case loses nothing either: the Assistant hosts
        the SAME `QueryRail` component, grouping included.
        ⚠ THE ONE THING THAT WAS NOT DUPLICATED IS THE HEADING ITSELF. That header rendered an
        `<h1>` its own comment called "the first time the work surface has NAMED itself"; deleting
        it outright would leave this page with no heading in the accessibility tree at all. So the
        name survives as an `h1` that costs NO vertical space, which is the half of the owner's
        instruction this surface can honour exactly. `flex: 1 1 auto` on `.shell-grid-host` hands
        the reclaimed band to the grid with no CSS change.
      */}
      <h1 className="lp-sr-only">
        {active.name || "Query"} on {active.source.label}
      </h1>
      <div className="shell-grid-host">
        <div className="qy-workspace">
          {hostedRail ? null : (
            <QueryRail groups={groups} activeId={active.id} onSelect={setActiveId}
              onDelete={(id) => void remove(id)} onChanged={upsert} />
          )}
          {/* ⛔ NO PROVENANCE ROW HERE (W35-T21, owner item 4 / R2). The "Sources" disclosure and
              the snapshot line it opened are deleted; a Query view is a database view now, and a
              database view carries no banner. The citations still travel — `binding` above is
              built from this artefact AND the fetched citation list — and the reader still SEES
              them in the chat, under the answer that used them (`AssistantPage::CitationLine`).
              ⚠ Do NOT paste that builder call into a comment: `verify_grid_ux.py`'s NC mutates the
              FIRST occurrence of it, so a second copy in prose keeps the scan green over a page
              that stopped binding citations [[prose-that-becomes-its-own-marker]]. */}
          <div className="qy-surface">
            <div className="qy-grid-host">
              <OverlayProvider>
                <CustomerGrid key={gridScopeFor(active.source.database)}
                  scope={gridScopeFor(active.source.database)} queryBinding={binding} />
              </OverlayProvider>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}
