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 { queryCitationLabel } from "../customer-grid/queryPreview";
import { DbHead, 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, QueryProvenance, 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);
  const cited = citations.filter((citation) => active.citationIds.includes(citation.id));
  const provenance = cited.map(queryCitationLabel).join(" · ")
    || `${active.source.label} · snapshot ${String(active.source.source_version)} · retrieved ${active.source.retrieved_at}`;
  return (
    <div className="shell-db-frame">
      <DbHead label={active.source.label} {...(entry?.icon ? { icon: entry.icon } : {})} />
      <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} />
          )}
          <div className="qy-surface">
            <QueryProvenance view={active} detail={provenance} />
            <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>
  );
}
