// LaunchPage — the Launch surface (Slice 4). PAGE tier: owns ALL fetching for the
// launch flow (composition rule #1) and drives the pure LaunchStepper + the pure
// window.LaunchModel helpers. One page, three modes chosen by the launch record:
//   • Gate    (launch === null, or an edit-config re-approval after a failure):
//             the review-and-approve column — station line summary, PM board
//             preview, the AL/GitHub/Railway/webhooks config, a secrets presence
//             panel, the no-rollback warning, and the Approve footer.
//   • Launching / failed: the LaunchStepper ladder + a status header; a failed
//             launch offers Retry (on the first failed step, inside the stepper)
//             and — when the failure asks the operator to rename the repo — an
//             "Edit configuration" path back to the gate, pre-filled from the
//             approved config.
//   • Live:   the completion screen — PM/GitHub/Railway links, the webhook + verify
//             checklists, and a read-only link back to the station line.
//
// Refresh: a SINGLE loop ONLY while launch.status === "launching" — SSE change
// pings (CreateStream.subscribeDraftChange, surface "launch") drive getLaunch
// refetches, 1.5s poll preserved as fallback — mirrors line-setup-page's hygiene
// (alive flag + cleanup on unmount / slug change). The three pure helpers live in lib/launch-model.js
// (window.LaunchModel), unit-tested in tests/launch-model.test.mjs. See COMPONENTS.md.

const LP_DRAFT_KEY = (slug) => `al-launch-draft:${slug}`;
function LP_readDraft(slug) {
  try { const raw = window.localStorage.getItem(LP_DRAFT_KEY(slug)); return raw ? JSON.parse(raw) : null; }
  catch { return null; }
}
function LP_writeDraft(slug, config) {
  try { window.localStorage.setItem(LP_DRAFT_KEY(slug), JSON.stringify(config)); } catch { /* quota / private mode */ }
}
function LP_clearDraft(slug) {
  try { window.localStorage.removeItem(LP_DRAFT_KEY(slug)); } catch { /* ignore */ }
}

// A positive-integer id (tolerant of the numeric strings a number <input> yields).
function LP_posInt(v) {
  if (v == null) return false;
  const n = typeof v === 'number' ? v : Number(String(v).trim());
  return Number.isInteger(n) && n > 0;
}

// "The operator typed something here." Blank is meaningful for the two OPTIONAL
// connections (PM Board, Railway): it means "not connected", not "invalid".
function LP_nonEmpty(v) {
  return v != null && String(v).trim() !== '';
}

// The two Teamwork boards are named in full on first mention, then by shorthand.
// The tooltips describe what the code actually does with each board:
//   • PM Board — optional. The launch writes one task per plan feature onto it
//     (pm_apply, tagged for idempotency) and registers the TASK.UPDATED/TASK.MOVED
//     webhooks there; a tagged task moved on that board is mirrored into the line.
//   • AL Board — the board the line itself runs on. al_instantiate creates its
//     project record and stations, and the conductor moves each task through the
//     stage columns as it advances, pausing on the human-review gates.
const PM_BOARD_TIP = 'Project Management (PM) Board — the customer-facing Teamwork project where work is ordered and decisions are made. Optional: connect one and Launch writes a task per feature onto it and registers the webhooks that feed tagged tasks into the line. Leave it blank and the line runs without a customer-facing board.';
const AL_BOARD_TIP = 'Assembly Line (AL) Board — the Teamwork project the line itself operates. Launch creates its project record and stations; the conductor moves each task through the stage columns as it advances and stops there for human review gates. Required.';

// ── small presentational helpers (local, not on window) ───────────────────────
// A hover tooltip on a small ⓘ circle. Native `title` so it needs no positioning
// logic and stays readable at any viewport; `tabIndex` keeps it keyboard-reachable.
const LP_Info = ({ tip }) => (
  <span
    title={tip}
    tabIndex={0}
    role="img"
    aria-label={tip}
    className="ml-1.5 inline-flex h-4 w-4 shrink-0 cursor-help items-center justify-center rounded-full border border-neutral-300 text-[10px] font-bold text-neutral-400 align-middle hover:border-neutral-400 hover:text-neutral-600"
  >i</span>
);

const LP_Section = ({ title, tip, children }) => (
  <section className="rounded-2xl border border-neutral-200 bg-white shadow-sm overflow-hidden">
    <div className="px-5 py-3 border-b border-neutral-100">
      <div className="text-[10px] font-bold uppercase tracking-widest text-neutral-400">{title}{tip ? <LP_Info tip={tip} /> : null}</div>
    </div>
    <div className="px-5 py-4">{children}</div>
  </section>
);

const LP_Field = ({ label, hint, children }) => (
  <label className="block">
    <div className="text-xs font-semibold text-neutral-600 mb-1">{label}</div>
    {children}
    {hint && <div className="text-xs text-neutral-400 mt-1">{hint}</div>}
  </label>
);

const LP_INPUT = 'w-full text-sm px-3 py-2 rounded-lg border border-neutral-300 focus:outline-none focus:ring-2 focus:ring-indigo-300';

const LP_PresenceRow = ({ ok, label }) => (
  <div className="flex items-center justify-between py-1.5">
    <span className="text-sm text-neutral-700">{label}</span>
    <span className={`inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-bold ${ok ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'}`}>
      {ok ? '✓ present' : '✗ missing'}
    </span>
  </div>
);

function LaunchPage({ slug }) {
  const LM = window.LaunchModel;

  const [state, setState] = React.useState('loading'); // loading | ready | error
  const [launch, setLaunch] = React.useState(null);
  const [defaults, setDefaults] = React.useState(null);
  const [context, setContext] = React.useState(null);
  const [latest, setLatest] = React.useState(null);     // { version, hash } for the approve payload
  const [lineGraph, setLineGraph] = React.useState(null);

  const [config, setConfig] = React.useState(null);
  const [preview, setPreview] = React.useState(null);
  const [previewing, setPreviewing] = React.useState(false);
  const [previewError, setPreviewError] = React.useState(null);

  const [approving, setApproving] = React.useState(false);
  const [approveError, setApproveError] = React.useState(null); // { kind, ... }
  const [editMode, setEditMode] = React.useState(false);
  const [draftMsg, setDraftMsg] = React.useState('');
  const [transport, setTransport] = React.useState('sse'); // 'sse' | 'poll' (fallback)

  // Bootstrap: the launch record + the line-setup latest version (for the approve
  // payload's graphVersion/graphHash). Restore the localStorage draft only when no
  // launch exists yet (the gate's initial config).
  React.useEffect(() => {
    let alive = true;
    setState('loading');
    setLaunch(null); setDefaults(null); setContext(null); setLatest(null); setLineGraph(null);
    setConfig(null); setPreview(null); setPreviewing(false); setPreviewError(null);
    setApproving(false); setApproveError(null); setEditMode(false); setDraftMsg('');
    Promise.all([
      window.Api.getLaunch(slug),
      window.Api.getLineSetup(slug).catch(() => null),
    ]).then(([lr, ls]) => {
      if (!alive) return;
      const versions = ls && ls.lineSetup && Array.isArray(ls.lineSetup.versions) ? ls.lineSetup.versions : [];
      const lv = versions.length ? versions[versions.length - 1] : null;
      setLaunch(lr.launch || null);
      setDefaults(lr.defaults || null);
      setContext(lr.context || null);
      setLatest(lv ? { version: lv.version, hash: lv.hash } : null);
      setLineGraph(lv ? lv.graph : null);
      const saved = lr.launch ? null : LP_readDraft(slug);
      setConfig(LM.configFromDefaults(lr.defaults, saved));
      setState('ready');
    }).catch(() => { if (alive) setState('error'); });
    return () => { alive = false; };
  }, [slug]);

  const launchStatus = launch ? launch.status : null;

  // The single refresh loop — only while launching. SSE change pings drive
  // refetches (the runner saves the record after every step transition);
  // the 1.5s poll remains the fallback transport.
  React.useEffect(() => {
    if (launchStatus !== 'launching') return;
    let alive = true;
    const refresh = () => { window.Api.getLaunch(slug).then((r) => { if (alive) setLaunch(r.launch || null); }).catch(() => {}); };
    if (transport === 'sse' && window.CreateStream) {
      const sub = window.CreateStream.subscribeDraftChange({
        slug,
        surface: 'launch',
        onChange: refresh,
        onFallback: () => setTransport('poll'),
      });
      return () => { alive = false; sub.close(); };
    }
    const iv = setInterval(refresh, 1500);
    return () => { alive = false; clearInterval(iv); };
  }, [slug, launchStatus, transport]);

  // Any config edit stales the preview: the board diff, repo-name check, and
  // webhook readiness were computed for the OLD values. Approve re-disables
  // (canApprove needs a truthy preview) until the operator re-previews.
  const setCfg = (patch) => { setConfig((c) => ({ ...c, ...patch })); setPreview(null); setPreviewError(null); };
  const setGithub = (patch) => { setConfig((c) => ({ ...c, github: { ...(c && c.github), ...patch } })); setPreview(null); setPreviewError(null); };

  // PM Board and Railway are optional connections: blank sends null, and the server
  // skips their launch steps rather than failing the walk.
  const buildPayloadConfig = () => ({
    ...LM.launchConfigForWire(config),
    alProjectId: Number(config.alProjectId),
    github: {
      owner: String((config.github && config.github.owner) || '').trim(),
      name: String((config.github && config.github.name) || '').trim(),
      visibility: (config.github && config.github.visibility) || 'private',
      useExisting: !!(config.github && config.github.useExisting),
    },
  });

  const onPreview = () => {
    // A blank PM id is legal — it previews a launch with no board write at all.
    if (previewing) return;
    if (LP_nonEmpty(config.pmProjectId) && !LP_posInt(config.pmProjectId)) return;
    setPreviewing(true); setPreviewError(null);
    window.Api.previewLaunch(slug, LM.launchConfigForWire(config).pmProjectId, LM.launchConfigForWire(config).railwayProjectId, LM.githubForWire(config))
      .then((res) => { setPreview(res); })
      .catch((e) => {
        const body = (e && e.body) || {};
        const status = e && e.status;
        if (status === 503) setPreviewError('Teamwork is disabled on the server — a preview can’t reach the PM Board. Leave the PM Board id blank to launch without one.');
        else if (status === 422) setPreviewError((body && body.error) ? `Preview rejected: ${body.error}.` : 'Preview failed validation.');
        else if (status === 409 && body.error === 'no_tasklist') setPreviewError('That PM Board has no task lists — create one in Teamwork, or leave the id blank to launch without a PM Board.');
        else setPreviewError((e && e.message) || 'Could not preview the board changes.');
      })
      .finally(() => setPreviewing(false));
  };

  const onApprove = () => {
    if (!LM.canApprove(preview, config) || !latest || approving) return;
    setApproving(true); setApproveError(null);
    window.Api.approveLaunch(slug, { graphVersion: latest.version, graphHash: latest.hash, config: buildPayloadConfig() })
      .then((res) => {
        LP_clearDraft(slug);
        setEditMode(false);
        setPreview(null);
        setLaunch(res.launch || null);
      })
      .catch((e) => {
        const body = (e && e.body) || {};
        const status = e && e.status;
        if (status === 400 && body.error === 'stale_graph') setApproveError({ kind: 'stale_graph' });
        else if (status === 422) setApproveError({ kind: 'validation', validation: body.validation });
        else if (status === 400 && body.error === 'config') setApproveError({ kind: 'config', field: body.field });
        else if (status === 409 && (body.error === 'busy' || body.error === 'already_live')) {
          setApproveError({ kind: body.error });
          // catch up to the authoritative record — flips us into launching/live.
          window.Api.getLaunch(slug).then((r) => setLaunch(r.launch || null)).catch(() => {});
        } else if (status === 503) setApproveError({ kind: 'unavailable', message: (e && e.message) });
        else if (status === 409 && body.error === 'no_tasklist') setApproveError({ kind: 'error', message: 'The PM project has no task lists — create one in Teamwork first.' });
        else setApproveError({ kind: 'error', message: (e && e.message) });
      })
      .finally(() => setApproving(false));
  };

  const onRetry = () => {
    window.Api.retryLaunch(slug)
      .then((res) => setLaunch(res.launch || null))
      .catch(() => { window.Api.getLaunch(slug).then((r) => setLaunch(r.launch || null)).catch(() => {}); });
  };

  const onSaveDraft = () => {
    LP_writeDraft(slug, config);
    setDraftMsg('Draft saved');
    setTimeout(() => setDraftMsg(''), 2500);
  };

  const onEditConfig = () => {
    setConfig(LM.configFromDefaults(defaults, (launch && launch.config) || null));
    setPreview(null);
    setPreviewError(null);
    setApproveError(null);
    setEditMode(true);
  };

  const onUseSuggestion = (name) => setGithub({ name });

  // ── loading / error ─────────────────────────────────────────────────────────
  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 launch</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 showGate = !launch || editMode;

  // ══ MODE 1 — GATE ═════════════════════════════════════════════════════════
  if (showGate) {
    const nodes = lineGraph && Array.isArray(lineGraph.nodes) ? lineGraph.nodes : [];
    const edges = lineGraph && Array.isArray(lineGraph.edges) ? lineGraph.edges : [];
    const reworkCount = edges.filter((e) => e && e.kind === 'rework').length;

    const board = preview && preview.board ? preview.board : null;
    const creates = board && Array.isArray(board.creates) ? board.creates : [];
    const skips = board && Array.isArray(board.skips) ? board.skips : [];
    const ghPreview = preview && preview.github ? preview.github : null;
    const webhooks = preview && preview.webhooks ? preview.webhooks : null;
    const sideEffects = preview && Array.isArray(preview.sideEffectPlan) ? preview.sideEffectPlan : [];
    // Preview refreshes the context/webhook readiness; MERGE it over the getLaunch
    // context rather than replacing it. Replacing meant any key the preview payload
    // happened to omit read as absent — which is how a configured webhook secret
    // flipped to "missing" on the first Preview click. Server-side presence is only
    // ever reported by the server; a preview must not be able to downgrade it.
    const ctx = { ...(context || {}), ...((preview && preview.context) || {}) };
    const publicWebhookUrl = (webhooks && webhooks.url) || ctx.publicWebhookUrl || null;

    // Blank PM id is a valid state (no board connected); only a malformed one blocks.
    const useExisting = !!(config.github && config.github.useExisting);
    const ghFullName = `${(config.github && config.github.owner) || '?'}/${(config.github && config.github.name) || '?'}`;
    const pmOk = !LP_nonEmpty(config.pmProjectId) || LP_posInt(config.pmProjectId);
    const approveReady = LM.canApprove(preview, config) && !!latest;
    // A config edit clears the preview on purpose — approval seals the plan the
    // operator actually reviewed, so the preview must match the CURRENT config.
    const disabledReason = !latest ? 'Set up a station line version first'
      : !preview ? 'Preview the launch first — it re-runs after any config change'
      : !approveReady ? 'Fill in every required field'
      : 'Approve and Launch';

    return (
      <div className="max-w-3xl mx-auto p-6 md:p-8">
        <div className="mb-5">
          {editMode ? (
            <button onClick={() => setEditMode(false)} className="text-sm font-semibold text-neutral-500 hover:text-neutral-800">← Back to launch progress</button>
          ) : (
            <button onClick={() => window.navigate('/')} className="text-sm font-semibold text-neutral-500 hover:text-neutral-800">← Overview</button>
          )}
          <h1 className="text-2xl font-bold text-neutral-800 mt-2">Launch</h1>
          <p className="text-sm text-neutral-500 mt-1">
            {editMode
              ? 'Editing the configuration after a failed launch — fix it, preview, and re-approve.'
              : 'Review what Launch will write and create, then approve. This runs against real external systems.'}
          </p>
        </div>

        <div className="space-y-4">
          {/* 1 — Station line */}
          <LP_Section title="Station line">
            {nodes.length ? (
              <>
                <div className="text-sm text-neutral-600 mb-2">
                  <span className="font-semibold text-neutral-800">{nodes.length}</span> station{nodes.length === 1 ? '' : 's'}
                  {reworkCount > 0 && <span className="text-neutral-400"> · {reworkCount} rework edge{reworkCount === 1 ? '' : 's'}</span>}
                </div>
                <div className="flex flex-wrap gap-1.5">
                  {nodes.map((n) => (
                    <span key={n.id} className="inline-flex items-center rounded-full bg-neutral-100 px-2.5 py-0.5 text-xs font-medium text-neutral-700">{n.title || n.id}</span>
                  ))}
                </div>
              </>
            ) : (
              <div className="text-sm text-neutral-400 italic">No station line version yet — set up the line before launching.</div>
            )}
          </LP_Section>

          {/* 2 — PM board */}
          <LP_Section title="Project Management (PM) Board" tip={PM_BOARD_TIP}>
            <div className="flex items-end gap-3">
              <div className="w-40">
                <LP_Field label="PM Board project id (optional)">
                  <input type="number" min="1" value={config.pmProjectId ?? ''} onChange={(e) => setCfg({ pmProjectId: e.target.value })} className={LP_INPUT} placeholder="e.g. 481234" />
                </LP_Field>
              </div>
              <button
                type="button"
                onClick={onPreview}
                disabled={!pmOk || previewing}
                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"
              >{previewing ? 'Previewing…' : 'Preview board changes'}</button>
            </div>
            {previewError && <div className="mt-3 rounded-lg bg-red-50 border border-red-200 p-3 text-sm text-red-800">{previewError}</div>}
            {board && (
              <div className="mt-4">
                {board.tasklist && <div className="text-xs text-neutral-500 mb-2">Tasklist <span className="font-semibold text-neutral-700">{board.tasklist}</span></div>}
                <div className="overflow-x-auto rounded-lg border border-neutral-200">
                  <table className="w-full text-sm">
                    <thead>
                      <tr className="bg-neutral-50 text-left text-[11px] font-bold uppercase tracking-wide text-neutral-400">
                        <th className="px-3 py-2">Task</th>
                        <th className="px-3 py-2">Station(s)</th>
                        <th className="px-3 py-2">Tag</th>
                      </tr>
                    </thead>
                    <tbody>
                      {creates.map((c) => (
                        <tr key={c.featureKey} className="border-t border-neutral-100">
                          <td className="px-3 py-2 text-neutral-800">{c.name}</td>
                          <td className="px-3 py-2 text-neutral-600">{Array.isArray(c.stations) ? c.stations.join(', ') : (c.stations || '')}</td>
                          <td className="px-3 py-2"><span className="inline-flex rounded-full bg-indigo-50 text-indigo-700 px-2 py-0.5 text-xs font-medium">{c.tag}</span></td>
                        </tr>
                      ))}
                      {skips.map((k) => (
                        <tr key={`skip-${k}`} className="border-t border-neutral-100 bg-amber-50">
                          <td className="px-3 py-2 text-amber-900 font-medium">{k}</td>
                          <td className="px-3 py-2 text-amber-700" colSpan={2}>already on board — will be skipped</td>
                        </tr>
                      ))}
                      {creates.length === 0 && skips.length === 0 && (
                        <tr><td className="px-3 py-4 text-neutral-400 italic" colSpan={3}>No board changes.</td></tr>
                      )}
                    </tbody>
                  </table>
                </div>
              </div>
            )}
            {!board && !previewError && <div className="mt-3 text-xs text-neutral-400">Enter the PM project id and preview to see the board changes, repo-name check, and webhook readiness.</div>}
          </LP_Section>

          {/* 3 — Assembly Line project */}
          <LP_Section title="Assembly Line (AL) Board" tip={AL_BOARD_TIP}>
            <div className="w-40">
              <LP_Field label="AL project id">
                <input type="number" min="1" value={config.alProjectId ?? ''} onChange={(e) => setCfg({ alProjectId: e.target.value })} className={LP_INPUT} placeholder="e.g. 902" />
              </LP_Field>
            </div>
            <div className="mt-2 text-xs text-neutral-500">Creates the project record and data dir; run execution arrives in a later slice.</div>
          </LP_Section>

          {/* 4 — GitHub repository */}
          <LP_Section title="GitHub repository">
            <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
              <LP_Field label="Owner">
                <input type="text" value={(config.github && config.github.owner) || ''} onChange={(e) => setGithub({ owner: e.target.value })} className={LP_INPUT} placeholder="org or user" />
              </LP_Field>
              <LP_Field label="Name">
                <input type="text" value={(config.github && config.github.name) || ''} onChange={(e) => setGithub({ name: e.target.value })} className={LP_INPUT} placeholder="repo-name" />
              </LP_Field>
              <LP_Field label="Visibility">
                <select value={(config.github && config.github.visibility) || 'private'} onChange={(e) => setGithub({ visibility: e.target.value })} className={LP_INPUT}>
                  <option value="private">Private</option>
                  <option value="public">Public</option>
                </select>
              </LP_Field>
            </div>
            {/* Create vs adopt. Ticking this makes Launch take over the repo at
                owner/name instead of creating it — the path for a project that IS
                an existing repo. */}
            <label className="mt-3 flex items-start gap-2 cursor-pointer">
              <input
                type="checkbox"
                checked={useExisting}
                onChange={(e) => setGithub({ useExisting: e.target.checked })}
                className="mt-0.5"
              />
              <span className="text-sm text-neutral-700">
                Use existing repository
                <span className="block text-xs text-neutral-500">
                  Launch adopts {ghFullName} instead of creating it. It must already exist and the token must be able to push to it.
                </span>
              </span>
            </label>

            {/* Availability, from the preview's GET-only check of the name you typed. */}
            {ghPreview && ghPreview.nameCheck === 'exists' && (
              <div className={`mt-3 rounded-lg p-3 text-sm flex items-center justify-between gap-3 ${useExisting ? 'bg-emerald-50 border border-emerald-200 text-emerald-800' : 'bg-amber-50 border border-amber-200 text-amber-800'}`}>
                <span>
                  {useExisting
                    ? `${ghFullName} exists and this token can push to it — Launch will adopt it.`
                    : `${ghFullName} already exists. Adopt it, or pick a different name.`}
                </span>
                {!useExisting && (
                  <button type="button" onClick={() => setGithub({ useExisting: true })} className="shrink-0 px-3 py-1 rounded-lg text-xs font-bold bg-amber-600 text-white hover:bg-amber-700">Use existing</button>
                )}
              </div>
            )}
            {ghPreview && ghPreview.nameCheck === 'no_access' && (
              <div className="mt-3 rounded-lg bg-red-50 border border-red-200 p-3 text-sm text-red-800 flex items-center justify-between gap-3">
                <span>{ghFullName} exists but this GitHub token cannot push to it.</span>
                {ghPreview.suggestion && (
                  <button type="button" onClick={() => onUseSuggestion(ghPreview.suggestion)} className="shrink-0 px-3 py-1 rounded-lg text-xs font-bold bg-red-600 text-white hover:bg-red-700">Use “{ghPreview.suggestion}”</button>
                )}
              </div>
            )}
            {ghPreview && ghPreview.nameCheck === 'available' && (
              useExisting ? (
                <div className="mt-3 rounded-lg bg-red-50 border border-red-200 p-3 text-sm text-red-800">
                  {ghFullName} does not exist, so there is nothing to adopt. Untick “use existing repository” to create it.
                </div>
              ) : (
                <div className="mt-3 text-xs font-semibold text-emerald-600">Repository name is available.</div>
              )
            )}
            {(!ghPreview || ghPreview.nameCheck === 'unchecked') && (
              <div className="mt-3 text-xs text-neutral-400">Availability is checked when you preview.</div>
            )}
          </LP_Section>

          {/* 5 — Railway */}
          <LP_Section title="Railway">
            <div className="w-64 max-w-full">
              <LP_Field label="Railway project id (optional)">
                <input type="text" value={config.railwayProjectId || ''} onChange={(e) => setCfg({ railwayProjectId: e.target.value })} className={LP_INPUT} placeholder="paste the Railway project id" />
              </LP_Field>
            </div>
            <div className="mt-2 text-xs text-neutral-500">Create the project in Railway first and paste its id — Launch only connects, it never creates Railway resources.</div>
          </LP_Section>

          {/* 6 — Webhooks */}
          <LP_Section title="Webhooks">
            <div className="text-sm text-neutral-700 break-all">
              <span className="text-xs font-semibold text-neutral-500">Endpoint</span>
              <div className="font-mono text-xs text-neutral-600 mt-0.5">{publicWebhookUrl || '(no public webhook URL configured)'}</div>
            </div>
            {webhooks && Array.isArray(webhooks.events) && webhooks.events.length > 0 && (
              <div className="mt-3 flex flex-wrap gap-1.5">
                {webhooks.events.map((ev) => (
                  <span key={ev} className="inline-flex items-center rounded-full bg-neutral-100 px-2.5 py-0.5 text-xs font-mono text-neutral-700">{ev}</span>
                ))}
              </div>
            )}
            {webhooks && webhooks.ready === false && (
              <div className="mt-3 rounded-lg bg-red-50 border border-red-200 p-3 text-sm text-red-800">The webhook endpoint isn’t reachable yet — Launch may not receive PM updates.</div>
            )}
            {!webhooks && <div className="mt-3 text-xs text-neutral-400">Webhook readiness is checked when you preview.</div>}
          </LP_Section>

          {/* 7 — Secrets & config */}
          <LP_Section title="Secrets & config">
            <LP_PresenceRow ok={!!ctx.teamwork} label="Teamwork credentials" />
            <LP_PresenceRow ok={!!ctx.github} label="GitHub token" />
            <LP_PresenceRow ok={!!ctx.webhookSecret} label="Webhook secret" />
            <div className="mt-2 text-[11px] text-neutral-400">Only presence is shown — secret values are never rendered.</div>
          </LP_Section>

          {/* 8 — Before you Launch */}
          <LP_Section title="Before you Launch">
            <div className="rounded-lg bg-amber-50 border border-amber-200 p-3 text-sm text-amber-900">
              Launch creates real external resources — and writes to the PM Board when one is connected. There is no rollback: a failed Launch resumes forward from the failed step, and anything already created stays until finished or removed by hand.
            </div>
            {sideEffects.length > 0 && (
              <div className="mt-3">
                <div className="text-[10px] font-bold uppercase tracking-widest text-neutral-400 mb-1.5">Launch will</div>
                <ul className="space-y-1">
                  {sideEffects.map((s, i) => (
                    <li key={i} className="flex items-start gap-2 text-sm text-neutral-700">
                      <span className="mt-1.5 w-1.5 h-1.5 rounded-full bg-neutral-400 shrink-0" />
                      <span>{s}</span>
                    </li>
                  ))}
                </ul>
              </div>
            )}
          </LP_Section>

          {/* approve error surfaces */}
          {approveError && approveError.kind === 'stale_graph' && (
            <div className="rounded-xl bg-red-50 border border-red-200 p-4 flex items-center justify-between gap-3">
              <div className="text-sm text-red-800">The station line changed since this version — re-open the line, re-save, and launch again.</div>
              <button onClick={() => window.navigate(`/projects/${slug}/line`)} className="shrink-0 px-3 py-1.5 rounded-lg text-xs font-bold bg-red-600 text-white hover:bg-red-700">Back to line</button>
            </div>
          )}
          {approveError && approveError.kind === 'validation' && (
            <div className="rounded-xl bg-red-50 border border-red-200 p-4">
              <div className="text-sm font-semibold text-red-800 mb-1">The line failed validation:</div>
              <ul className="list-disc list-inside text-sm text-red-700 space-y-0.5">
                {(Array.isArray(approveError.validation && approveError.validation.errors) ? approveError.validation.errors : [])
                  .map((v, i) => <li key={i}>{(v && (v.message || v.code)) || String(v)}</li>)}
                {(!approveError.validation || !Array.isArray(approveError.validation.errors) || approveError.validation.errors.length === 0) && <li>Validation failed — re-check the station line.</li>}
              </ul>
            </div>
          )}
          {approveError && approveError.kind === 'config' && (
            <div className="rounded-xl bg-red-50 border border-red-200 p-4 text-sm text-red-800">The server rejected the configuration{approveError.field ? <> — check <span className="font-semibold">{approveError.field}</span></> : ''}.</div>
          )}
          {approveError && approveError.kind === 'busy' && (
            <div className="rounded-xl bg-amber-50 border border-amber-200 p-4 text-sm text-amber-900">A launch is already in progress — catching up…</div>
          )}
          {approveError && approveError.kind === 'already_live' && (
            <div className="rounded-xl bg-emerald-50 border border-emerald-200 p-4 text-sm text-emerald-800">This project is already live — catching up…</div>
          )}
          {approveError && approveError.kind === 'unavailable' && (
            <div className="rounded-xl bg-red-50 border border-red-200 p-4 text-sm text-red-800">A required service is unavailable ({approveError.message || '503'}). Try again shortly.</div>
          )}
          {approveError && approveError.kind === 'error' && (
            <div className="rounded-xl bg-red-50 border border-red-200 p-4 text-sm text-red-800">{approveError.message || 'Approve failed — please try again.'}</div>
          )}

          {/* footer actions. Buttons never wrap internally (whitespace-nowrap,
              shrink-0) — the disabled reason is the flexible element and gets
              its own full-width line above the row, so a long reason squeezes
              nothing. */}
          {!approveReady && !approving && (
            <div className="text-xs font-medium text-neutral-500 text-right pt-2">{disabledReason}</div>
          )}
          <div className="flex flex-wrap items-center justify-between gap-3 pt-2">
            <div className="flex items-center gap-2">
              <button onClick={() => window.navigate('/')} className="shrink-0 whitespace-nowrap px-4 py-2 text-sm font-semibold rounded-lg text-neutral-500 hover:text-neutral-800">Cancel</button>
              <button onClick={() => window.navigate(`/projects/${slug}/line`)} className="shrink-0 whitespace-nowrap px-4 py-2 text-sm font-semibold rounded-lg bg-white border border-neutral-300 text-neutral-700 hover:bg-neutral-50">Back to line</button>
            </div>
            <div className="flex items-center gap-2">
              {draftMsg && <span className="text-xs font-medium text-emerald-600 whitespace-nowrap">{draftMsg}</span>}
              <button onClick={onSaveDraft} className="shrink-0 whitespace-nowrap px-4 py-2 text-sm font-semibold rounded-lg bg-white border border-neutral-300 text-neutral-700 hover:bg-neutral-50">Save draft</button>
              <button
                onClick={onApprove}
                disabled={!approveReady || approving}
                title={disabledReason}
                className="shrink-0 whitespace-nowrap px-5 py-2 text-sm font-semibold rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 shadow-md shadow-indigo-600/20 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none"
              >{approving ? 'Launching…' : 'Approve and Launch'}</button>
            </div>
          </div>
        </div>
      </div>
    );
  }

  // ══ MODE 3 — LIVE ═════════════════════════════════════════════════════════
  if (launch.status === 'live') {
    const byId = {};
    (Array.isArray(launch.steps) ? launch.steps : []).forEach((s) => { if (s && s.id) byId[s.id] = s; });
    const cfg = launch.config || {};
    const ctx = context || {};
    const ghUrl = byId.github_repo && byId.github_repo.result && byId.github_repo.result.htmlUrl;
    const rwUrl = byId.railway_connect && byId.railway_connect.result && byId.railway_connect.result.url;
    const pmUrl = ctx.twSiteUrl && cfg.pmProjectId != null ? `${ctx.twSiteUrl}/app/projects/${cfg.pmProjectId}` : null;
    const hooks = byId.webhooks && byId.webhooks.result && Array.isArray(byId.webhooks.result.hooks) ? byId.webhooks.result.hooks : [];
    const checks = byId.verify && byId.verify.result && Array.isArray(byId.verify.result.checks) ? byId.verify.result.checks : [];
    const approval = launch.approval || null;

    const LinkRow = ({ label, href }) => (
      <div className="flex items-center justify-between py-2 border-t border-neutral-100 first:border-t-0">
        <span className="text-sm text-neutral-600">{label}</span>
        {href ? (
          <a href={href} target="_blank" rel="noopener noreferrer" className="text-sm font-semibold text-indigo-600 hover:text-indigo-700 inline-flex items-center gap-1 break-all">Open <window.ArrowRight size={14} /></a>
        ) : (
          <span className="text-xs text-neutral-400 italic">unavailable</span>
        )}
      </div>
    );

    return (
      <div className="max-w-3xl mx-auto p-6 md:p-8">
        <div className="mb-5">
          <button onClick={() => window.navigate('/')} className="text-sm font-semibold text-neutral-500 hover:text-neutral-800">← Overview</button>
        </div>

        <div className="rounded-2xl bg-emerald-50 border border-emerald-200 p-5 flex items-center gap-3 mb-4">
          <window.CheckCircle size={28} className="text-emerald-500 shrink-0" />
          <div>
            <div className="text-lg font-bold text-emerald-800">Live</div>
            <div className="text-sm text-emerald-700">
              The line is launched.{approval && approval.operator ? <> Approved by <span className="font-semibold">{approval.operator}</span>{approval.at ? <> · {new Date(approval.at).toLocaleString()}</> : null}.</> : null}
            </div>
          </div>
        </div>

        <div className="space-y-4">
          <LP_Section title="Links">
            <LinkRow label="PM Board" href={pmUrl} />
            <LinkRow label="GitHub repository" href={ghUrl} />
            <LinkRow label="Railway project" href={rwUrl} />
          </LP_Section>

          {hooks.length > 0 && (
            <LP_Section title="Webhooks">
              <div className="flex flex-wrap gap-1.5">
                {hooks.map((h, i) => (
                  <span key={h.id || i} className="inline-flex items-center gap-1.5 rounded-full bg-neutral-100 px-2.5 py-1 text-xs text-neutral-700">
                    <span className="font-mono">{h.event}</span>
                    <span className={`rounded px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide ${h.action === 'created' ? 'bg-emerald-200 text-emerald-800' : 'bg-neutral-200 text-neutral-600'}`}>{h.action}</span>
                  </span>
                ))}
              </div>
            </LP_Section>
          )}

          <LP_Section title="Verification">
            {checks.length ? (
              <ul className="space-y-2">
                {checks.map((c, i) => (
                  <li key={i} className="flex items-start gap-2">
                    {c.ok ? <window.CheckCircle size={16} className="text-emerald-500 mt-0.5 shrink-0" /> : <window.AlertCircle size={16} className="text-red-500 mt-0.5 shrink-0" />}
                    <div className="min-w-0">
                      <div className={`text-sm font-medium ${c.ok ? 'text-neutral-800' : 'text-red-700'}`}>{c.name}</div>
                      {c.detail && <div className="text-xs text-neutral-500">{c.detail}</div>}
                    </div>
                  </li>
                ))}
              </ul>
            ) : (
              <div className="text-sm text-neutral-400 italic">No verification checks recorded.</div>
            )}
          </LP_Section>

          {/* The live board is the PRIMARY next stop. This used to offer only the
              station line, which is a locked read-only record post-launch — landing
              there straight after a successful launch reads like the flow stalled. */}
          <div className="rounded-2xl border border-neutral-200 bg-white p-5 flex items-center justify-between gap-3">
            <div className="text-sm text-neutral-600">The line is live. Work moves on the board from here.</div>
            <div className="shrink-0 flex items-center gap-2">
              <button onClick={() => window.navigate(`/projects/${slug}/line`)} className="px-4 py-2 text-sm font-semibold rounded-lg bg-white border border-neutral-300 text-neutral-700 hover:bg-neutral-50">Station line (read-only)</button>
              <button onClick={() => window.navigate(`/projects/${slug}`)} className="px-4 py-2 text-sm font-semibold rounded-lg bg-emerald-600 text-white hover:bg-emerald-700">Open the live board →</button>
            </div>
          </div>
        </div>
      </div>
    );
  }

  // ══ MODE 2 — LAUNCHING / FAILED ═══════════════════════════════════════════
  const failed = launch.status === 'failed';
  const failedStep = failed ? LM.firstFailedStep(launch.steps) : null;
  const canEditConfig = !!(failedStep && Array.isArray(failedStep.log) && failedStep.log.some((l) => /edit the repository name/i.test(String(l))));
  const approval = launch.approval || null;

  return (
    <div className="max-w-3xl mx-auto p-6 md:p-8">
      <div className="mb-5">
        <button onClick={() => window.navigate('/')} className="text-sm font-semibold text-neutral-500 hover:text-neutral-800">← Overview</button>
      </div>

      {failed ? (
        <div className="rounded-2xl bg-red-50 border border-red-200 p-5 mb-4 flex items-center gap-3">
          <window.AlertCircle size={28} className="text-red-500 shrink-0" />
          <div className="min-w-0 flex-1">
            <div className="text-lg font-bold text-red-800">Launch failed</div>
            <div className="text-sm text-red-700">It stopped at a step below. Fix the cause, then retry from the failed step — Launch resumes forward.</div>
          </div>
          {canEditConfig && (
            <button onClick={onEditConfig} className="shrink-0 px-3 py-1.5 rounded-lg text-xs font-bold bg-white border border-red-300 text-red-700 hover:bg-red-100">Edit configuration</button>
          )}
        </div>
      ) : (
        <div className="rounded-2xl bg-indigo-50 border border-indigo-200 p-5 mb-4 flex items-center gap-3">
          <span className="relative flex w-3 h-3 shrink-0">
            <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-3 w-3 bg-indigo-500" />
          </span>
          <div>
            <div className="text-lg font-bold text-indigo-800">Launching…</div>
            <div className="text-sm text-indigo-700">Running the launch steps.{approval && approval.operator ? <> Approved by <span className="font-semibold">{approval.operator}</span>.</> : null}</div>
          </div>
        </div>
      )}

      <div className="rounded-2xl border border-neutral-200 bg-white shadow-sm p-5">
        <window.LaunchStepper steps={launch.steps} onRetry={failed ? onRetry : null} />
      </div>
    </div>
  );
}

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