/* Engineering — the single pane over the AI-native SDLC. Assigns window.LedgerEngineering.

   Three views in phase 1: Overview (the whole open queue), Approvals (the slice
   this person can actually act on) and Repos (what each governed repo looks like
   right now). Enhancements and metrics are later phases and are deliberately
   absent rather than stubbed.

   THE ONE RULE WORTH KNOWING BEFORE EDITING THIS FILE: nothing here decides
   whether an action is allowed. Every row arrives from pr-action carrying a
   `verdict` computed by preconditions.ts — the same function the action path
   re-evaluates before it touches GitHub. This file renders that verdict and
   nothing else. Do not add a condition here that gates a button; add it to
   preconditions.ts, where it is tested and where the server will enforce it.
   Both defects this build has found were a second copy of a rule drifting from
   the first, and this is the most expensive place for a third to appear.

   The board refreshes itself while anything is still in flight (checks running,
   mergeability unresolved, a just-acted row settling) and stops when everything
   has settled. It has to: `update_branch` returns 202 Accepted, so the row the
   action responds with can legitimately be the pre-action one.

   The portal also never holds a permission GitHub does not. An action goes out
   under the caller's own GitHub token (linked via github-oauth), so the review on
   the PR is theirs. If GitHub refuses, the refusal is shown verbatim and the
   portal stops — it never retries a merge on its own. */
(function () {
  const DS = window.LawCrimeDesignSystem_e18d68;
  const icons = () => setTimeout(() => window.lucide && window.lucide.createIcons(), 30);

  const ACTION_LABEL = {
    approve: "Approve",
    merge: "Merge",
    update_branch: "Update branch",
    request_review: "Request review",
    wait: "Waiting",
    none: "—",
  };
  const ACTION_ICON = {
    approve: "check-check",
    merge: "git-merge",
    update_branch: "arrow-down-up",
    request_review: "user-plus",
    wait: "loader",
    none: "minus",
  };

  function ago(iso) {
    if (!iso) return "—";
    const mins = Math.round((Date.now() - Date.parse(iso)) / 60000);
    if (!Number.isFinite(mins)) return "—";
    if (mins < 1) return "just now";
    if (mins < 60) return `${mins}m ago`;
    const h = Math.round(mins / 60);
    if (h < 48) return `${h}h ago`;
    return `${Math.round(h / 24)}d ago`;
  }

  function hours(n) {
    if (n == null) return "—";
    if (n < 24) return `${n}h`;
    return `${Math.round(n / 24)}d`;
  }

  const shortSha = (s) => (s ? String(s).slice(0, 7) : "—");
  const firstLine = (s) => String(s || "").split("\n")[0];

  // ---------------------------------------------------------------- pieces
  function TierBadge({ tier }) {
    if (!tier) return <span className="eng-tier eng-tier--none" title="The PR body does not state a tier">No tier</span>;
    return <span className={`eng-tier eng-tier--${String(tier).toLowerCase()}`}>{tier}</span>;
  }

  /* Checks as one glance: green when every reported check passed, red on any
     failure, amber while anything is still running. The count is there because
     "green" with 0 checks is not the same thing as green. */
  function Checks({ row }) {
    const { checks_total: total, checks_success: ok, checks_failed: bad, checks_pending: pending } = row;
    const tone = bad > 0 ? "bad" : (pending > 0 || total === 0) ? "wait" : "good";
    const icon = bad > 0 ? "x-circle" : (pending > 0 || total === 0) ? "clock" : "check-circle-2";
    const title = total === 0 ? "No checks have reported on this head" : `${ok}/${total} passed${pending ? `, ${pending} running` : ""}${bad ? `, ${bad} failed` : ""}`;
    return (
      <span className={`eng-checks eng-checks--${tone}`} title={title}>
        <i data-lucide={icon} /> {total === 0 ? "none" : `${ok}/${total}`}
      </span>
    );
  }

  function Reviewers({ row }) {
    const list = row.requested_reviewers || [];
    if (row.approvals > 0) {
      return <span className="eng-rev eng-rev--ok" title={`${row.approvals} approval(s) on the current head`}>
        <i data-lucide="user-check" /> {row.approvals} approved
      </span>;
    }
    if (row.changes_requested > 0) {
      return <span className="eng-rev eng-rev--bad"><i data-lucide="user-x" /> changes requested</span>;
    }
    if (!list.length) {
      return <span className="eng-rev eng-rev--none" title="Nobody has been asked to review this"><i data-lucide="user-round-x" /> nobody asked</span>;
    }
    return <span className="eng-rev" title={list.join(", ")}><i data-lucide="user-round" /> {list.join(", ")}</span>;
  }

  /* The action button IS the verdict. `primary` says what to offer; `reason`
     says why when there is nothing to offer, and it is shown rather than hidden
     because "this button is grey" without a reason is the thing that makes people
     go round the portal and do it on GitHub. */
  function ActionButton({ row, busy, onAct }) {
    const v = row.verdict || { primary: "none", reason: "", allowed: [], blocked: {} };
    const primary = v.primary;
    const live = primary === "approve" || primary === "merge" || primary === "update_branch";
    const cls = `eng-act eng-act--${primary}${live ? "" : " eng-act--off"}`;
    return (
      <button
        className={cls}
        disabled={!live || busy}
        title={live ? v.reason || ACTION_LABEL[primary] : (v.reason || "No action available")}
        onClick={() => live && onAct(primary)}
      >
        <i data-lucide={busy ? "loader" : ACTION_ICON[primary] || "minus"} />
        {busy ? "Working…" : ACTION_LABEL[primary] || "—"}
      </button>
    );
  }

  function PrRow({ row, busy, onAct, onRefresh, expanded, onToggle }) {
    const v = row.verdict || {};
    // 48h old with nobody asked to review is the queue stalling, which is the
    // failure mode this portal exists to make visible.
    const stale = row.age_hours >= 48 && (row.requested_reviewers || []).length === 0 && row.approvals === 0;
    return (
      <>
        <tr className={stale ? "eng-row eng-row--stale" : "eng-row"}>
          <td className="eng-c-repo">
            {stale && <span className="eng-dot" title="Open over 48h with no reviewer requested" />}
            {String(row.repo || "").split("/").pop()}
          </td>
          <td className="eng-c-title">
            <a href={row.html_url} target="_blank" rel="noopener noreferrer">
              #{row.number} {row.title}
            </a>
            {row.enhancement_id && <span className="eng-enh">{row.enhancement_id}</span>}
            {row.draft && <span className="eng-draft">draft</span>}
          </td>
          <td>{row.author_login || "—"}</td>
          <td><TierBadge tier={row.tier} /></td>
          <td title={row.opened_at ? new Date(row.opened_at).toLocaleString() : ""}>{hours(row.age_hours)}</td>
          <td><Checks row={row} /></td>
          <td><Reviewers row={row} /></td>
          <td className="eng-c-act">
            <ActionButton row={row} busy={busy} onAct={onAct} />
            <button className="eng-icon" title="Re-read this PR from GitHub" disabled={busy} onClick={onRefresh}>
              <i data-lucide="refresh-cw" />
            </button>
            <button className="eng-icon" title={expanded ? "Hide detail" : "Why?"} onClick={onToggle}>
              <i data-lucide={expanded ? "chevron-up" : "chevron-down"} />
            </button>
          </td>
        </tr>
        {expanded && (
          <tr className="eng-detail">
            <td colSpan={8}>
              <div className="eng-detail__grid">
                <div>
                  <div className="eng-detail__h">Verdict</div>
                  <p className="eng-detail__reason">{v.reason || "—"}</p>
                  <ul className="eng-blocked">
                    {Object.entries(v.blocked || {}).map(([k, why]) => (
                      <li key={k}><b>{ACTION_LABEL[k] || k}</b> — {why}</li>
                    ))}
                    {!Object.keys(v.blocked || {}).length && <li>Nothing is blocked.</li>}
                  </ul>
                </div>
                <div>
                  <div className="eng-detail__h">Branch rule</div>
                  <dl className="eng-kv">
                    <dt>Protected</dt><dd>{row.gated ? "yes" : "no rule on the base branch"}</dd>
                    <dt>Approvals required</dt><dd>{row.required_approvals ?? "—"}</dd>
                    <dt>Required checks</dt><dd>{(row.required_checks || []).join(", ") || "none"}</dd>
                    <dt>Reported on head</dt><dd>{(row.check_names || []).join(", ") || "none"}</dd>
                    <dt>Mergeability</dt><dd>{row.mergeable_state || "not read yet"}</dd>
                    <dt>Head</dt><dd>{shortSha(row.head_sha)} on <code>{row.head_ref}</code> → <code>{row.base_ref}</code></dd>
                  </dl>
                </div>
              </div>
            </td>
          </tr>
        )}
      </>
    );
  }

  // -------------------------------------------------------------- identity
  /* Link / unlink this person's GitHub account. Nothing in the portal can act as
     a person until this is done, so an unlinked user gets this banner instead of
     a board full of dead buttons with no explanation. */
  function IdentityBar({ identity, onChanged, onError }) {
    const { Button } = DS;
    const [busy, setBusy] = React.useState(false);
    const linked = identity && identity.linked;

    async function link() {
      setBusy(true);
      try {
        const r = await window.LedgerAPI.githubLink("start");
        // A full navigation, not a popup: GitHub refuses to render the consent
        // screen in a frame, and a blocked popup is a dead button with no error.
        window.location.href = r.url;
      } catch (e) { onError(e.message || String(e)); setBusy(false); }
    }
    async function unlink() {
      if (!window.confirm("Unlink your GitHub account? The stored token is deleted and you will not be able to approve or merge from the portal until you link again.")) return;
      setBusy(true);
      try { await window.LedgerAPI.githubLink("revoke"); await onChanged(); }
      catch (e) { onError(e.message || String(e)); }
      finally { setBusy(false); }
    }

    if (linked) {
      const id = identity.identity || {};
      return (
        <div className="eng-ident eng-ident--ok">
          <i data-lucide="github" />
          <span>Acting as <b>@{id.gh_login}</b>{id.token_expires_at ? ` · token renews automatically` : ""}</span>
          <button className="eng-linkbtn" disabled={busy} onClick={unlink}>Unlink</button>
        </div>
      );
    }
    return (
      <div className="eng-ident eng-ident--warn">
        <i data-lucide="github" />
        <span>
          <b>Link your GitHub account</b> to approve and merge from here. Actions run as
          you, not as a bot, so the review on the pull request carries your name.
        </span>
        <Button variant="primary" icon="link" disabled={busy} onClick={link}>
          {busy ? "Opening GitHub…" : "Link GitHub"}
        </Button>
      </div>
    );
  }

  // -------------------------------------------------------------- the app
  function Engineering({ view = "eng-overview" }) {
    const { Card, StatTile, TapeBar } = DS;
    const [rows, setRows] = React.useState(null);
    const [actor, setActor] = React.useState({ gh_login: null, linked: false });
    const [identity, setIdentity] = React.useState(null);
    const [health, setHealth] = React.useState(null);
    const [repos, setRepos] = React.useState([]);
    const [merged, setMerged] = React.useState([]);
    const [err, setErr] = React.useState(null);
    const [msg, setMsg] = React.useState(null);
    const [busyKey, setBusyKey] = React.useState(null);
    const [open, setOpen] = React.useState({});

    const key = (r) => `${r.repo}#${r.number}`;

    const loadBoard = React.useCallback(async () => {
      try {
        const b = await window.LedgerAPI.engBoard();
        setRows(b.rows);
        setActor(b.actor);
      } catch (e) { setErr(e.message || String(e)); setRows([]); }
    }, []);

    const loadIdentity = React.useCallback(async () => {
      try { setIdentity(await window.LedgerAPI.githubLink("status")); }
      catch (e) { setIdentity({ linked: false }); setErr(e.message || String(e)); }
    }, []);

    React.useEffect(() => { loadBoard(); loadIdentity(); }, [loadBoard, loadIdentity]);

    // THE BOARD IS A MIRROR OF A SYSTEM THAT KEEPS MOVING, so a page that only
    // loads once is wrong within a minute. Checks finish, someone else reviews,
    // and `update_branch` answers 202 — accepted, not done — so even the row you
    // just acted on settles a moment after the response comes back. Without this
    // the label sits on "Waiting" until someone presses Refresh, which reads as a
    // hung action rather than a stale page. Observed on Ledger#5, 2026-09-11.
    //
    // Polling only while something is actually in flight: a board where every row
    // has settled is not refetched, so an idle tab costs nothing.
    // A DRAFT is excluded deliberately. Its mergeability may stay `unknown`
    // indefinitely — GitHub has no reason to compute it for a PR nobody can merge
    // — so counting it as in-flight would poll for a state that never arrives.
    const inFlight = (rows || []).some((r) => {
      if (r.draft) return false;
      const p = r.verdict && r.verdict.primary;
      return p === "wait" || r.checks_pending > 0 || r.mergeable_state === "unknown" || r.mergeable_state == null;
    });
    React.useEffect(() => {
      if (!inFlight || busyKey) return;          // never poll over an action in progress
      const t = setTimeout(() => { loadBoard(); }, 10000);
      return () => clearTimeout(t);
    }, [inFlight, busyKey, rows, loadBoard]);
    React.useEffect(() => {
      window.LedgerAPI.engIngestHealth().then(setHealth).catch(() => setHealth(null));
      window.LedgerAPI.engRepos().then(setRepos).catch(() => setRepos([]));
      const since = new Date(Date.now() - 7 * 864e5).toISOString();
      window.LedgerAPI.engMerged(since, 50).then(setMerged).catch(() => setMerged([]));
    }, []);
    React.useEffect(() => { icons(); });

    // github-oauth 302s the browser back here with either `linked=<login>` or
    // `link_error=<why>` in the hash — it renders nothing itself, because
    // Supabase serves HTML from the functions domain as text/plain and a page
    // there cannot be read. The portal is the only place that can show this in
    // the app's own chrome. Say it once, then clear the hash so a reload does
    // not repeat it.
    React.useEffect(() => {
      const hash = window.location.hash || "";
      const ok = /[?&]linked=([^&]+)/.exec(hash);
      const bad = /[?&]link_error=([^&]+)/.exec(hash);
      if (!ok && !bad) return;
      if (ok) setMsg({ ok: true, text: `Linked as @${decodeURIComponent(ok[1])}.` });
      else setErr(`GitHub link failed: ${decodeURIComponent(bad[1])}`);
      window.history.replaceState(null, "", window.location.pathname + window.location.search + "#eng-overview");
      loadIdentity();
      loadBoard();
    }, [loadIdentity, loadBoard]);

    async function act(row, action) {
      setBusyKey(key(row)); setErr(null); setMsg(null);
      try {
        let extra = {};
        if (action === "request_review") {
          const who = window.prompt(`Request a review on ${row.repo}#${row.number} from which GitHub login?`);
          if (!who) { setBusyKey(null); return; }
          extra = { reviewers: [who.trim()] };
        }
        const res = await window.LedgerAPI.prAction(action, row.repo, row.number, extra);
        setMsg({
          ok: true,
          text: res.auto_merge
            ? `${row.repo}#${row.number} will merge when its checks pass.`
            : `${ACTION_LABEL[action]} done on ${row.repo}#${row.number}.`,
        });
        await loadBoard();
      } catch (e) {
        // A relink is a different remedy from a refusal, and saying "failed" for
        // both sends people to GitHub to do by hand what one click would fix.
        if (e.relink) { setErr(`${e.message} — link your GitHub account again below.`); await loadIdentity(); }
        else setErr(e.message || String(e));
        await loadBoard();
      } finally { setBusyKey(null); }
    }

    async function refreshOne(row) {
      setBusyKey(key(row)); setErr(null);
      try {
        await window.LedgerAPI.prAction("refresh", row.repo, row.number);
        await loadBoard();
      } catch (e) { setErr(e.message || String(e)); }
      finally { setBusyKey(null); }
    }

    const all = rows || [];
    const actionable = all.filter((r) => {
      const p = r.verdict && r.verdict.primary;
      return p === "approve" || p === "merge" || p === "update_branch";
    });
    const blocked = all.filter((r) => {
      const v = r.verdict || {};
      return v.primary === "none" || (v.primary === "wait" && r.checks_failed > 0);
    });
    const mergedThisWeek = merged.length;

    function Table({ list, empty }) {
      if (!list.length) return <div className="eng-empty">{empty}</div>;
      return (
        <div className="eng-tablewrap">
          <table className="eng-table">
            <thead>
              <tr>
                <th>Repo</th><th>Pull request</th><th>Author</th><th>Tier</th>
                <th>Age</th><th>Checks</th><th>Review</th><th></th>
              </tr>
            </thead>
            <tbody>
              {list.map((r) => (
                <PrRow
                  key={key(r)}
                  row={r}
                  busy={busyKey === key(r)}
                  expanded={!!open[key(r)]}
                  onToggle={() => setOpen((s) => ({ ...s, [key(r)]: !s[key(r)] }))}
                  onAct={(a) => act(r, a)}
                  onRefresh={() => refreshOne(r)}
                />
              ))}
            </tbody>
          </table>
        </div>
      );
    }

    const title = view === "eng-approvals" ? "Approvals" : view === "eng-repos" ? "Repos" : "Overview";

    return (
      <div className="eng">
        <div className="page-head">
          <div>
            <div className="eyebrow">Engineering · {title}</div>
            <h1 className="page-title">{title}</h1>
          </div>
          <div className="eng-head-right">
            <button className="eng-linkbtn" onClick={() => { setRows(null); loadBoard(); }}>
              <i data-lucide="refresh-cw" /> Refresh
            </button>
          </div>
        </div>

        {err && <div className="login__err" style={{ margin: "12px 0" }}>{err}</div>}
        {msg && <div className="ai-saved" style={{ margin: "12px 0" }}>{msg.text}</div>}

        <IdentityBar identity={identity} onChanged={async () => { await loadIdentity(); await loadBoard(); }} onError={setErr} />

        {view !== "eng-repos" && (
          <>
            <div className="eng-tiles">
              <StatTile value={all.length} label="Open pull requests" />
              <StatTile value={actionable.length} label="You can act on" />
              <StatTile value={blocked.length} label="Blocked" />
              <StatTile value={mergedThisWeek} label="Merged this week" />
            </div>

            {health && (
              <div className={`eng-health${health.errored > 0 ? " eng-health--bad" : ""}`}>
                <i data-lucide={health.errored > 0 ? "alert-triangle" : "activity"} />
                <span>
                  {health.deliveries_24h} webhook deliveries in 24h · {health.errored} errored ·{" "}
                  {health.unprocessed} unprocessed · last event {ago(health.last_received_at)} ·{" "}
                  last nightly walk {ago(health.last_backfill_at)}
                </span>
              </div>
            )}
          </>
        )}

        {view === "eng-overview" && (
          <Card>
            <Card.Body>
              <TapeBar size="sm">OPEN QUEUE</TapeBar>
              <Table list={all} empty={rows === null ? "Loading…" : "No open pull requests on the governed repos."} />
            </Card.Body>
          </Card>
        )}

        {view === "eng-approvals" && (
          <Card>
            <Card.Body>
              <TapeBar size="sm">WAITING ON YOU</TapeBar>
              <Table
                list={actionable}
                empty={
                  rows === null ? "Loading…"
                    : !actor.linked ? "Link your GitHub account above — until then the portal cannot act as you."
                    : all.length === 0 ? "Nothing is open."
                    : "Nothing is waiting on you. Every open PR is either yours, still running its checks, or blocked — open the Overview to see why."
                }
              />
            </Card.Body>
          </Card>
        )}

        {view === "eng-repos" && (
          <ReposView repos={repos} rows={all} merged={merged} />
        )}
      </div>
    );
  }

  /* One card per governed repo: what protects it, what is open on it, and what
     has landed on its default branch. The ruleset is shown next to the checks
     that actually reported, because a renamed CI check silently ungates a repo
     and that mismatch is invisible anywhere else. */
  function ReposView({ repos, rows, merged }) {
    const { Card } = DS;
    const [commits, setCommits] = React.useState({});

    React.useEffect(() => {
      let live = true;
      Promise.all((repos || []).filter((r) => r.governed).map((r) =>
        window.LedgerAPI.engCommits(r.gh_repo_id, r.default_branch || "main", 12)
          .then((c) => [r.gh_repo_id, c]).catch(() => [r.gh_repo_id, []])))
        .then((pairs) => { if (live) setCommits(Object.fromEntries(pairs)); });
      return () => { live = false; };
    }, [repos]);
    React.useEffect(() => { icons(); }, [commits, repos]);

    if (!repos || !repos.length) return <div className="eng-empty">No repos registered yet — the nightly walk populates this.</div>;

    return (
      <div className="eng-repos">
        {repos.filter((r) => r.governed).map((r) => {
          const rs = r.ruleset || {};
          const openPrs = (rows || []).filter((p) => p.repo === r.full_name);
          const landed = (merged || []).filter((p) => p.repo_id === r.gh_repo_id);
          const protectedBranch = !!rs.pull_request_required;
          return (
            <Card key={r.gh_repo_id} className="eng-repo">
              <Card.Body>
                <div className="eng-repo__head">
                  <a href={r.html_url} target="_blank" rel="noopener noreferrer" className="eng-repo__name">
                    {r.full_name}
                  </a>
                  {protectedBranch
                    ? <span className="eng-rule eng-rule--on" title={`${rs.required_approvals} approval(s); ${(rs.required_checks || []).length} required check(s)`}>
                        <i data-lucide="shield-check" /> {rs.required_approvals} approval{rs.required_approvals === 1 ? "" : "s"}
                      </span>
                    : <span className="eng-rule eng-rule--off" title="No pull-request rule on the default branch — the portal will not offer a merge here">
                        <i data-lucide="shield-off" /> No ruleset
                      </span>}
                  <span className="eng-repo__meta">
                    {openPrs.length} open · {landed.length} merged this week · walked {ago(r.last_backfill_at)}
                  </span>
                </div>

                {protectedBranch && (
                  <div className="eng-repo__rules">
                    Required checks: {(rs.required_checks || []).join(", ") || "none"}
                    {rs.dismiss_stale_reviews ? " · approvals dismissed on push" : ""}
                    {rs.strict_up_to_date ? " · branch must be up to date" : ""}
                  </div>
                )}

                <ul className="eng-commits">
                  {(commits[r.gh_repo_id] || []).map((c) => (
                    <li key={c.sha}>
                      <a href={c.html_url} target="_blank" rel="noopener noreferrer"><code>{shortSha(c.sha)}</code></a>
                      <span className="eng-commits__msg">{firstLine(c.message)}</span>
                      <span className="eng-commits__by">{c.author_login || c.author_name || "—"}</span>
                      {c.pr_number && <span className="eng-commits__pr">#{c.pr_number}</span>}
                      <span className="eng-commits__at">{ago(c.committed_at)}</span>
                    </li>
                  ))}
                  {!(commits[r.gh_repo_id] || []).length && <li className="eng-empty">No commits on {r.default_branch} in the window.</li>}
                </ul>
              </Card.Body>
            </Card>
          );
        })}
      </div>
    );
  }

  window.LedgerEngineering = Engineering;
})();
