// LineSetupPage — the line-setup workspace (Slice 4). PAGE tier: owns ALL fetching
// for the surface (composition rule #1) and feeds the pure composites (LineCanvas,
// StationPanel) their props. Two shapes, chosen by whether a line has been proposed:
//   • no versions yet  → a hero: plan summary + "Generate station graph"
//                         (POST proposal, then poll getLineSetup every 2s while
//                          proposal.status === "running")
//   • ≥1 version        → the inspect/EDIT workspace (canvas + inspector + footer)
// Task 8 makes the workspace editable: the page owns a WORKING copy of the latest
// graph, applies ops immutably via window.GraphEdit, tracks a `dirty` flag, and
// saves with optimistic-concurrency (baseVersion) via Api.saveLineGraph. The SERVER
// validation from the save response is authoritative for the footer chip / Continue
// gate; window.GraphEdit.quickIssues drives the canvas badges between saves.
// See COMPONENTS.md.

// ── plan summary + generate hero ──────────────────────────────────────────────
const LSP_Hero = ({ plan, running, error, onGenerate }) => {
  const features = (plan && Array.isArray(plan.features)) ? plan.features : [];
  return (
    <div className="max-w-3xl mx-auto p-8">
      <div className="rounded-2xl border border-neutral-200 bg-white shadow-sm overflow-hidden">
        <div className="px-6 py-5 border-b border-neutral-100">
          <div className="text-[10px] font-bold uppercase tracking-widest text-neutral-400 mb-1">Line setup</div>
          <h1 className="text-2xl font-bold text-neutral-800">{(plan && plan.title) || 'Untitled project'}</h1>
          {plan && plan.deadline && (
            <div className="mt-1 text-sm text-neutral-500">Deadline <span className="font-semibold text-neutral-700">{plan.deadline}</span></div>
          )}
        </div>

        <div className="px-6 py-5">
          <div className="text-xs font-bold uppercase tracking-widest text-neutral-400 mb-2">
            Features ({features.length})
          </div>
          {features.length ? (
            <ul className="space-y-1.5 mb-6">
              {features.map((f) => (
                <li key={f.key} className="flex items-center gap-2 text-sm text-neutral-700">
                  <span className="w-1.5 h-1.5 rounded-full bg-indigo-400 shrink-0" />
                  <span className="truncate">{f.title || f.key}</span>
                  {f.phase && <span className="text-[10px] uppercase tracking-wide text-neutral-400">{f.phase}</span>}
                </li>
              ))}
            </ul>
          ) : (
            <div className="text-sm text-neutral-400 italic mb-6">No features in the plan yet.</div>
          )}

          {error && (
            <div className="mb-4 rounded-lg bg-red-50 border border-red-200 p-3 text-sm text-red-800">{error}</div>
          )}

          {running ? (
            <div className="flex items-center gap-3 text-neutral-600">
              <span className="relative flex w-2.5 h-2.5">
                <span className="absolute inline-flex h-full w-full rounded-full bg-indigo-500 opacity-75 animate-ping" />
                <span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-indigo-500" />
              </span>
              <span className="font-semibold">Proposing station line…</span>
            </div>
          ) : (
            <button
              onClick={onGenerate}
              className="px-4 py-2 rounded-lg font-semibold text-sm bg-indigo-600 text-white hover:bg-indigo-700 shadow-md shadow-indigo-600/20 active:scale-95"
            >{error ? 'Try again' : 'Generate station graph'}</button>
          )}
        </div>
      </div>
    </div>
  );
};

// ── inspector default: graph overview + clickable validation issues + palette ──
const LSP_Overview = ({ graph, issues, onSelect, templates, onAdd, canEdit }) => {
  const nodes = (graph && Array.isArray(graph.nodes)) ? graph.nodes : [];
  const edges = (graph && Array.isArray(graph.edges)) ? graph.edges : [];
  const flow = edges.filter((e) => e.kind !== 'rework').length;
  const rework = edges.filter((e) => e.kind === 'rework').length;
  const palette = Array.isArray(templates) ? templates : [];
  return (
    <div className="flex flex-col h-full">
      <div className="px-4 py-3 border-b border-neutral-200">
        <div className="font-bold text-neutral-800">Station line</div>
        <div className="text-xs text-neutral-400 mt-0.5">Select a station to edit it{canEdit ? ', or add one below.' : '.'}</div>
      </div>
      <div className="px-4 py-3 border-b border-neutral-100 grid grid-cols-3 gap-2 text-center">
        <div>
          <div className="text-lg font-bold text-neutral-800">{nodes.length}</div>
          <div className="text-[10px] uppercase tracking-widest text-neutral-400">Stations</div>
        </div>
        <div>
          <div className="text-lg font-bold text-slate-600">{flow}</div>
          <div className="text-[10px] uppercase tracking-widest text-neutral-400">Flow</div>
        </div>
        <div>
          <div className="text-lg font-bold text-orange-600">{rework}</div>
          <div className="text-[10px] uppercase tracking-widest text-neutral-400">Rework</div>
        </div>
      </div>
      <div className="flex-1 min-h-0 overflow-y-auto">
        <div className="px-4 py-3 border-b border-neutral-100">
          <div className="text-[10px] font-bold uppercase tracking-widest text-neutral-400 mb-2">
            Validation ({issues.length})
          </div>
          {issues.length ? (
            <ul className="space-y-1.5">
              {issues.map((it, i) => (
                <li key={i}>
                  <button
                    onClick={() => it.nodeId && onSelect(it.nodeId)}
                    disabled={!it.nodeId}
                    className={`w-full text-left rounded-lg border p-2 text-xs flex items-start gap-2 ${it.sev === 'error' ? 'border-red-200 bg-red-50' : 'border-amber-200 bg-amber-50'} ${it.nodeId ? 'hover:shadow-sm cursor-pointer' : 'cursor-default'}`}
                  >
                    <span className={`shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide ${it.sev === 'error' ? 'bg-red-500 text-white' : 'bg-amber-400 text-amber-900'}`}>
                      {it.sev}
                    </span>
                    <span className={it.sev === 'error' ? 'text-red-800' : 'text-amber-800'}>
                      {it.message}
                      {it.nodeId && <span className="block mt-0.5 font-mono text-[10px] text-neutral-400">{it.nodeId}</span>}
                    </span>
                  </button>
                </li>
              ))}
            </ul>
          ) : (
            <div className="rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-800 font-semibold">
              No validation issues — ready to launch.
            </div>
          )}
        </div>

        {canEdit && palette.length > 0 && (
          <div className="px-4 py-3">
            <div className="text-[10px] font-bold uppercase tracking-widest text-neutral-400 mb-2">
              Add a station
            </div>
            <ul className="space-y-1.5">
              {palette.map((t) => (
                <li key={t.id} className="flex items-start gap-2 rounded-lg border border-neutral-200 p-2">
                  <div className="min-w-0 flex-1">
                    <div className="text-sm font-semibold text-neutral-700 truncate">{t.title || t.id}</div>
                    {t.description && <div className="text-xs text-neutral-500 mt-0.5">{t.description}</div>}
                  </div>
                  <button
                    onClick={() => onAdd(t)}
                    className="shrink-0 w-7 h-7 flex items-center justify-center rounded-lg bg-indigo-600 text-white text-lg font-bold leading-none hover:bg-indigo-700 active:scale-95"
                    title={`Add ${t.title || t.id}`}
                    aria-label={`Add ${t.title || t.id} station`}
                  >+</button>
                </li>
              ))}
            </ul>
          </div>
        )}
      </div>
    </div>
  );
};

function LineSetupPage({ slug }) {
  const [state, setState] = React.useState('loading'); // loading | ready | error
  const [data, setData] = React.useState(null);        // { lineSetup, status, plan }
  const [templates, setTemplates] = React.useState([]);
  const [launchExists, setLaunchExists] = React.useState(false);
  // The launch STATUS, not just its existence: once it is live this page must point
  // forward to the running board instead of back at a Launch gate it already passed.
  const [launchStatus, setLaunchStatus] = React.useState(null);
  const [selectedId, setSelectedId] = React.useState(null);
  const [generating, setGenerating] = React.useState(false);
  const [genError, setGenError] = React.useState(null);
  const [transport, setTransport] = React.useState('sse'); // 'sse' | 'poll' (fallback)

  // Task-8 editing state: a WORKING copy of the latest graph the operator mutates.
  const [working, setWorking] = React.useState(null);
  const [baseVersion, setBaseVersion] = React.useState(0);
  const [dirty, setDirty] = React.useState(false);
  const [saveState, setSaveState] = React.useState('idle'); // idle | saving | error
  const [saveError, setSaveError] = React.useState(null);
  const [stale, setStale] = React.useState(null);           // null | { latest }

  // Bootstrap: line-setup + templates + whether a launch record already exists.
  React.useEffect(() => {
    let alive = true;
    setState('loading'); setData(null); setSelectedId(null); setGenerating(false); setGenError(null);
    setWorking(null); setBaseVersion(0); setDirty(false); setSaveState('idle'); setSaveError(null); setStale(null);
    Promise.all([
      window.Api.getLineSetup(slug),
      window.Api.getLineTemplates().catch(() => ({ templates: [] })),
      window.Api.getLaunch(slug).then((r) => (r && r.launch ? r.launch.status || 'exists' : null)).catch(() => null),
    ]).then(([ls, tpl, status]) => {
      if (!alive) return;
      setData(ls);
      setTemplates(Array.isArray(tpl && tpl.templates) ? tpl.templates : []);
      setLaunchExists(status != null);
      setLaunchStatus(status);
      setState('ready');
    }).catch(() => { if (alive) setState('error'); });
    return () => { alive = false; };
  }, [slug]);

  const proposalStatus = data && data.lineSetup && data.lineSetup.proposal ? data.lineSetup.proposal.status : null;

  // Refresh while a proposal runs: the server's change pings drive refetches
  // (initial ping on open covers anything missed before subscribing); the 2s
  // poll remains the fallback transport.
  React.useEffect(() => {
    if (state !== 'ready' || proposalStatus !== 'running') return;
    let alive = true;
    const refresh = () => { window.Api.getLineSetup(slug).then((d) => { if (alive) setData(d); }).catch(() => {}); };
    if (transport === 'sse' && window.CreateStream) {
      const sub = window.CreateStream.subscribeDraftChange({
        slug,
        surface: 'line',
        onChange: refresh,
        onFallback: () => setTransport('poll'),
      });
      return () => { alive = false; sub.close(); };
    }
    const iv = setInterval(refresh, 2000);
    return () => { alive = false; clearInterval(iv); };
  }, [state, slug, proposalStatus, transport]);

  // Derived: the server's latest version (authoritative for validation/versioning).
  const versionsAll = data && data.lineSetup && Array.isArray(data.lineSetup.versions) ? data.lineSetup.versions : [];
  const latestAll = versionsAll.length ? versionsAll[versionsAll.length - 1] : null;
  const latestVersionNum = latestAll ? latestAll.version : null;

  // Sync the working copy whenever a NEW latest version appears (proposal completes,
  // a save lands, or a reload after a stale conflict). Keyed on the version number
  // so ordinary edits (which mint no new server version) never clobber the copy.
  React.useEffect(() => {
    if (!latestAll) { setWorking(null); return; }
    setWorking(JSON.parse(JSON.stringify(latestAll.graph)));
    setBaseVersion(latestAll.version);
    setDirty(false);
    setStale(null);
    setSaveError(null);
    setSaveState('idle');
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [slug, latestVersionNum]);

  const onGenerate = () => {
    setGenerating(true); setGenError(null);
    window.Api.generateLineProposal(slug)
      .then(() => window.Api.getLineSetup(slug))   // pick up proposal.status === "running"
      .then((d) => { setData(d); })
      .catch((e) => setGenError((e && e.message) || 'Could not start the proposal'))
      .finally(() => setGenerating(false));
  };

  // Apply one edit op through window.GraphEdit[op.type], immutably. Ops that don't
  // change the graph (addEdge dedupe / self-edge) return the SAME reference — we
  // detect that and leave `dirty` untouched.
  const applyOp = (graph, op) => {
    const fn = window.GraphEdit[op.type];
    if (typeof fn !== 'function') return graph;
    switch (op.type) {
      case 'rename': return fn(graph, op.id, op.title);
      case 'setInstructions': return fn(graph, op.id, op.text);
      case 'setTemplate': return fn(graph, op.id, op.templateId);
      case 'toggleHumanReview': return fn(graph, op.id);
      case 'setCovers': return fn(graph, op.id, op.keys);
      case 'addNode': return fn(graph, op.template);
      case 'removeNode': return fn(graph, op.id);
      case 'addEdge': return fn(graph, op.edge);
      case 'removeEdge': return fn(graph, op.edge);
      default: return graph;
    }
  };

  const onEdit = (op) => {
    if (!working || launchExists || saveState === 'saving') return;
    const next = applyOp(working, op);
    if (next === working) return; // no-op (duplicate / self edge)
    setWorking(next);
    setDirty(true);
    if (op.type === 'removeNode' && op.id === selectedId) setSelectedId(null);
    if (op.type === 'addNode') {
      const added = next.nodes[next.nodes.length - 1];
      if (added) setSelectedId(added.id);
    }
  };

  const onSave = () => {
    if (!working || saveState === 'saving' || launchExists) return;
    setSaveState('saving'); setSaveError(null);
    const saved = working;
    window.Api.saveLineGraph(slug, saved, baseVersion)
      .then((res) => {
        // Append the returned version data + the working graph to the local list so
        // the footer/issues refresh without a refetch; the sync effect then adopts
        // it as the new latest (working resync, baseVersion bump, dirty cleared).
        setData((d) => {
          const ls = (d && d.lineSetup) || {};
          const versions = Array.isArray(ls.versions) ? ls.versions : [];
          const entry = { version: res.version, hash: res.hash, validation: res.validation, graph: saved, createdBy: 'operator' };
          return { ...d, lineSetup: { ...ls, versions: [...versions, entry] } };
        });
        setSaveState('idle');
      })
      .catch((e) => {
        const body = e && e.body;
        if (e && e.status === 409 && body && body.error === 'stale') {
          setStale({ latest: body.latest });
          setSaveState('idle');
        } else if (e && e.status === 409 && body && body.error === 'locked') {
          setLaunchExists(true);
          setSaveError('This line is locked — Launch has been approved.');
          setSaveState('error');
        } else {
          setSaveError((e && e.message) || 'Save failed — please try again.');
          setSaveState('error');
        }
      });
  };

  const onReload = () => {
    window.Api.getLineSetup(slug).then((d) => {
      setData(d);
      const vs = d && d.lineSetup && Array.isArray(d.lineSetup.versions) ? d.lineSetup.versions : [];
      const lt = vs.length ? vs[vs.length - 1] : null;
      if (lt) { setWorking(JSON.parse(JSON.stringify(lt.graph))); setBaseVersion(lt.version); }
      setDirty(false); setStale(null); setSaveError(null); setSaveState('idle');
    }).catch(() => {});
  };

  const guardedNavigate = (to) => {
    if (dirty && !window.confirm('You have unsaved changes. Leave without saving?')) return;
    window.navigate(to);
  };

  if (state === 'loading') return <div className="p-16 text-center text-neutral-400">Loading…</div>;
  if (state === 'error') {
    return (
      <div className="max-w-xl mx-auto p-16 text-center">
        <div className="text-lg font-semibold text-neutral-600 mb-1">Could not load the line setup</div>
        <button onClick={() => window.navigate('/')} className="mt-4 px-4 py-2 rounded-lg font-semibold text-sm bg-neutral-800 text-white hover:bg-neutral-900">← Back</button>
      </div>
    );
  }

  const running = proposalStatus === 'running' || generating;

  // ── hero: no versions yet ───────────────────────────────────────────────────
  if (versionsAll.length === 0) {
    const heroError = genError || (proposalStatus === 'errored'
      ? ((data.lineSetup && data.lineSetup.proposal && data.lineSetup.proposal.error) || 'The proposal failed.')
      : null);
    return <LSP_Hero plan={data.plan} running={running} error={heroError} onGenerate={onGenerate} />;
  }

  // ── workspace: ≥1 version ───────────────────────────────────────────────────
  const canEdit = !launchExists;
  // While a save is in flight the version-keyed sync effect will adopt the
  // saved snapshot and clobber anything typed meanwhile — lock the panel.
  const editable = canEdit && saveState !== 'saving';
  // Canvas + inspector render the WORKING copy; validation/versioning read the
  // authoritative server latest.
  const graph = working || (latestAll && latestAll.graph) || { nodes: [], edges: [] };
  const serverValidation = (latestAll && latestAll.validation) || { errors: [], warnings: [] };
  const errors = Array.isArray(serverValidation.errors) ? serverValidation.errors : [];
  const warnings = Array.isArray(serverValidation.warnings) ? serverValidation.warnings : [];

  const errorsByNode = {};
  for (const e of errors) if (e && e.nodeId) errorsByNode[e.nodeId] = (errorsByNode[e.nodeId] || 0) + 1;
  // Between saves the server validation is stale relative to edits — show the pure
  // client subset; once clean/saved, fall back to the authoritative error map.
  const issuesByNode = dirty ? window.GraphEdit.quickIssues(graph) : errorsByNode;

  const issues = [
    ...errors.map((e) => ({ sev: 'error', message: e.message || e.code || 'Error', nodeId: e.nodeId || null })),
    ...warnings.map((w) => ({ sev: 'warning', message: w.message || w.code || 'Warning', nodeId: w.nodeId || null })),
  ];

  const selectedNode = selectedId ? (graph.nodes || []).find((n) => n.id === selectedId) : null;
  const selectedTemplate = selectedNode ? templates.find((t) => t.id === selectedNode.templateId) : undefined;

  const validationChip = errors.length > 0
    ? { cls: 'bg-red-100 text-red-700', text: `${errors.length} error${errors.length === 1 ? '' : 's'} · ${warnings.length} warning${warnings.length === 1 ? '' : 's'}` }
    : warnings.length > 0
      ? { cls: 'bg-amber-100 text-amber-700', text: `0 errors · ${warnings.length} warning${warnings.length === 1 ? '' : 's'}` }
      : { cls: 'bg-emerald-100 text-emerald-700', text: 'Ready to launch' };

  const canSave = dirty && canEdit && saveState !== 'saving' && !stale;
  const canContinue = !dirty && errors.length === 0;

  return (
    <div className="flex flex-col h-[calc(100vh-3.25rem)]">
      {launchExists && (
        <div className="px-4 py-2 bg-neutral-800 text-neutral-100 text-sm font-medium flex items-center gap-2">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
            <rect x="3" y="11" width="18" height="11" rx="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" />
          </svg>
          Locked — the station line is read-only once Launch is approved.
        </div>
      )}

      {stale && (
        <div className="px-4 py-2 bg-amber-100 text-amber-900 text-sm font-medium flex items-center justify-between gap-3">
          <span>Someone saved a newer version — reload</span>
          <button
            onClick={onReload}
            className="shrink-0 px-3 py-1 rounded-lg text-xs font-bold bg-amber-500 text-white hover:bg-amber-600"
          >Reload</button>
        </div>
      )}

      <div className="grid grid-cols-1 lg:grid-cols-[1fr_26rem] flex-1 min-h-0">
        <section className="min-w-0 min-h-0 overflow-hidden border-r border-neutral-200 relative">
          <window.LineCanvas
            graph={graph}
            selectedId={selectedId}
            onSelect={(id) => setSelectedId(id)}
            issuesByNode={issuesByNode}
            readOnly={launchExists}
          />
        </section>
        <aside className="min-w-0 bg-white flex flex-col min-h-0">
          {selectedNode ? (
            <window.StationPanel
              key={selectedNode.id}
              node={selectedNode}
              template={selectedTemplate}
              templates={templates}
              features={(data.plan && data.plan.features) || []}
              edges={graph.edges || []}
              nodes={graph.nodes || []}
              readOnly={!editable}
              onEdit={onEdit}
              onClose={() => setSelectedId(null)}
            />
          ) : (
            <LSP_Overview
              graph={graph}
              issues={issues}
              onSelect={(id) => setSelectedId(id)}
              templates={templates}
              onAdd={(t) => onEdit({ type: 'addNode', template: t })}
              canEdit={editable}
            />
          )}
        </aside>
      </div>

      <div className="border-t border-neutral-200 bg-white px-4 py-3 flex items-center justify-between gap-3">
        <div className="flex items-center gap-3 min-w-0">
          <button
            onClick={() => guardedNavigate('/')}
            className="shrink-0 text-sm font-semibold text-neutral-500 hover:text-neutral-800"
          >← Overview</button>
          <span className={`rounded-full px-3 py-1 text-xs font-bold ${validationChip.cls}`}>{validationChip.text}</span>
          {dirty && <span className="text-xs text-neutral-400 truncate">Unsaved changes</span>}
          {saveState === 'error' && saveError && <span className="text-xs text-red-600 truncate">{saveError}</span>}
        </div>
        <div className="flex items-center gap-2">
          <button
            onClick={onSave}
            disabled={!canSave}
            title={stale ? 'Reload the newer version first' : (canEdit ? 'Save the station graph' : 'Locked')}
            className="px-4 py-2 text-sm font-semibold rounded-lg bg-white border border-neutral-300 text-neutral-700 hover:bg-neutral-50 disabled:opacity-50 disabled:cursor-not-allowed"
          >{saveState === 'saving' ? 'Saving…' : 'Save graph'}</button>
          {/* Post-launch this page is a read-only record of a line that already
              shipped, so the forward action is the running board — not a Launch
              gate the project has been through. */}
          {launchStatus === 'live' ? (
            <button
              onClick={() => guardedNavigate(`/projects/${slug}`)}
              title="This line is launched — open the live board"
              className="px-4 py-2 text-sm font-semibold rounded-lg bg-emerald-600 text-white hover:bg-emerald-700"
            >Launched — open the live board →</button>
          ) : (
            <button
              disabled={!canContinue}
              onClick={() => guardedNavigate(`/projects/${slug}/launch`)}
              title={canContinue ? 'Continue to Launch' : 'Save a graph with no blocking errors first'}
              className="px-4 py-2 text-sm font-semibold rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed"
            >Continue to Launch →</button>
          )}
        </div>
      </div>
    </div>
  );
}

if (typeof window !== 'undefined') {
  window.LineSetupPage = LineSetupPage;
}
