import type { Adapter } from "../adapter";
import type { AdapterResult, Item, Panel } from "../schema";
import { createGhHttp } from "./gh-http";
import {
  mergeRepoRuns,
  readRunHistory,
  recentRepoRuns,
  writeRunHistory,
  type RunHistoryStore,
  type StoredGhRun,
} from "../ghci-run-history";

export type GhRunner = (args: string[]) => Promise<string>;

export interface GhciAdapterOptions {
  id?: string;
  interval?: number;
  repos?: string[];
  runsPerRepo?: number;
  runGh?: GhRunner;
  now?: () => number;
  runHistoryStore?: RunHistoryStore;
  persistRunHistory?: boolean;
}

interface GhRun {
  id: number;
  run_attempt: number;
  run_number: number;
  name: string;
  head_branch: string;
  head_sha: string;
  display_title: string;
  status: string;
  conclusion: string | null;
  workflow_id: number;
  html_url: string;
  created_at: string;
  updated_at: string;
}

interface GhRunsResponse {
  total_count: number;
  workflow_runs: GhRun[];
}

interface GhRunnerEntry {
  id: number;
  name: string;
  status: string;
  busy: boolean;
}

interface GhRunnersResponse {
  total_count: number;
  runners: GhRunnerEntry[];
}

interface GhJob {
  id: number;
  name: string;
  status: string;
  conclusion: string | null;
  runner_id: number | null;
  runner_name: string | null;
  started_at: string | null;
  completed_at: string | null;
}

interface GhJobsResponse {
  total_count: number;
  jobs: GhJob[];
}

interface CheckNode {
  __typename: "CheckRun" | "StatusContext";
  name?: string;
  status?: string;
  conclusion?: string | null;
  context?: string;
  state?: string;
}

interface OpenPrNode {
  number: number;
  title: string;
  body: string;
  url: string;
  headRefName: string;
  headRefOid: string;
  baseRefName: string;
  createdAt: string;
  mergeable: string;
  mergeStateStatus: string;
  commits?: {
    nodes?: Array<{
      commit?: {
        statusCheckRollup?: {
          contexts?: { nodes?: CheckNode[]; pageInfo?: { hasNextPage?: boolean } };
        } | null;
      };
    }>;
  };
}

interface OpenSnapshot {
  data?: {
    repository?: {
      defaultBranchRef?: { name?: string } | null;
      refs?: {
        nodes?: Array<{ name: string; target?: { oid?: string } }>;
        pageInfo?: { hasNextPage?: boolean; endCursor?: string | null };
      };
      pullRequests?: {
        nodes?: OpenPrNode[];
        pageInfo?: { hasNextPage?: boolean; endCursor?: string | null };
      };
    };
  };
}

type OpenRepository = NonNullable<NonNullable<OpenSnapshot["data"]>["repository"]>;

interface MergedSnapshot {
  data?: {
    search?: {
      issueCount: number;
      pageInfo?: { hasNextPage?: boolean };
      nodes?: unknown[];
    };
  };
}

interface MergedPrNode {
  number: number;
  body: string;
  url: string;
  headRefName: string;
  headRefOid: string;
  createdAt: string;
  mergedAt: string;
}

interface MergedTimestampNode {
  createdAt: string;
  mergedAt: string;
}

interface PollRepo {
  repo: string;
  owner: string;
  name: string;
  row: RepoPanel;
  historyOk: boolean;
  queuedOk: boolean;
  inProgressOk: boolean;
  runnersOk: boolean;
  openOk: boolean;
  due: boolean;
  defaultBranch: string | null;
  refs: Array<{ name: string; target?: { oid?: string } }>;
  prs: OpenPrNode[];
  identities: TrainIdentity[];
  exactRuns: Map<string, GhRun[]>;
}

interface TrainIdentity {
  branch: string;
  oid: string;
  ref: boolean;
  openPr: OpenPrNode | null;
  mergedPr: MergedPrNode | null;
}

interface FleetRunner {
  id: number;
  name: string;
  status: string;
  busy: boolean;
  scope: string;
  currentJob: string | null;
  currentRepo: string | null;
  currentKnown: boolean;
}

interface CiPanelData {
  repos: RepoPanel[];
  runners: FleetRunner[];
  runnersComplete: boolean;
  oldestQueuedAgeH: number | null;
}

interface RepoPanel {
  repo: string;
  queueDepth: number | null;
  runs: Array<Record<string, unknown>>;
  runners: Array<Record<string, unknown>>;
  prs: Array<Record<string, unknown>>;
  trains: Array<Record<string, any>>;
  degraded: Array<"budget" | "pagination" | "jobs">;
  queueComplete: boolean;
  historyComplete: boolean;
  trainRunsComplete: boolean;
  runnersComplete: boolean;
  refsComplete: boolean;
  prsComplete: boolean;
  trainsComplete: boolean;
  pollDeferred: boolean;
  prQueueDepth: number | null;
  oldestQueuedAgeH: number | null;
  runnersBusy: number | null;
  runnersTotal: number | null;
  lag: {
    landsToday: number | null;
    medianOpenToMergeH7d: number | null;
    gates: Array<{ runId: number; computeMin: number; wallMin: number }>;
    warmedAt: string | null;
  };
}

interface AmplificationEntry {
  runId: number;
  computeMin: number;
  wallMin: number;
  completedAtMs: number;
}

interface Cadence {
  counter: number;
  due: boolean;
}

const DEFAULT_REPOS = ["alexcodeplace/multideal"];
const DEFAULT_RUNS_PER_REPO = 10;
const MAX_CALLS_PER_POLL = 52;
const MAX_BASE_REPOS = 6;
const HISTORY_PAGE_SIZE = 100;
const MAX_OPEN_PAGES = 10;
const TRAIN_PREFIX = "integration/batch-train-";
const INFRA_PATTERN = /ENOSPC|ENOTEMPTY|No space left|runner.*lost|The self-hosted runner.*lost communication/i;
const LOG_TAIL_BYTES = 65_536;

const OPEN_QUERY = `query CiOpenRepoSnapshot($owner:String!,$name:String!){repository(owner:$owner,name:$name){defaultBranchRef{name} refs(refPrefix:"refs/heads/", query:"integration/batch-train-", first:100, orderBy:{field:ALPHABETICAL,direction:ASC}) { nodes { name target { oid } } pageInfo { hasNextPage endCursor } } pullRequests(states:OPEN, first:100, orderBy:{field:CREATED_AT,direction:ASC}) { nodes { number title body url headRefName headRefOid baseRefName createdAt mergeable mergeStateStatus commits(last:1) { nodes { commit { statusCheckRollup { contexts(first:100) { nodes { __typename ... on CheckRun { name status conclusion } ... on StatusContext { context state } } pageInfo { hasNextPage } } } } } } } pageInfo { hasNextPage endCursor } }}}`;

const OPEN_REFS_PAGE_QUERY = `query CiOpenRefsPage($owner:String!,$name:String!,$after:String!){repository(owner:$owner,name:$name){refs(refPrefix:"refs/heads/", query:"integration/batch-train-", first:100, after:$after, orderBy:{field:ALPHABETICAL,direction:ASC}){nodes{name target{oid}}pageInfo{hasNextPage endCursor}}}}`;

const OPEN_PRS_PAGE_QUERY = `query CiOpenPrsPage($owner:String!,$name:String!,$after:String!){repository(owner:$owner,name:$name){pullRequests(states:OPEN, first:100, after:$after, orderBy:{field:CREATED_AT,direction:ASC}){nodes{number title body url headRefName headRefOid baseRefName createdAt mergeable mergeStateStatus commits(last:1){nodes{commit{statusCheckRollup{contexts(first:100){nodes{__typename ... on CheckRun{name status conclusion}... on StatusContext{context state}}pageInfo{hasNextPage}}}}}}}pageInfo{hasNextPage endCursor}}}}`;

function storedRuns(
  store: RunHistoryStore,
  repo: string,
  limit: number,
): GhRun[] {
  return recentRepoRuns(store, repo, limit) as GhRun[];
}

function applyStoredRuns(
  row: RepoPanel,
  store: RunHistoryStore,
  repo: string,
  limit: number,
): boolean {
  const runs = storedRuns(store, repo, limit);
  if (runs.length === 0) return false;
  row.runs = runs.map(panelRun);
  row.historyComplete = true;
  return true;
}

function syncRunHistory(
  row: RepoPanel,
  store: RunHistoryStore,
  repo: string,
  incoming: GhRun[] | null,
  syncedAt: string,
  limit: number,
): boolean {
  if (incoming) mergeRepoRuns(store, repo, incoming as StoredGhRun[], syncedAt);
  return applyStoredRuns(row, store, repo, limit);
}

interface OpenRepositorySnapshot {
  defaultBranch: string | null;
  refs: Array<{ name: string; target?: { oid?: string } }>;
  prs: OpenPrNode[];
  refsComplete: boolean;
  prsComplete: boolean;
  openOk: boolean;
}

async function loadOpenRepository(
  request: <T>(args: string[]) => Promise<T | null>,
  owner: string,
  name: string,
  canRequest: () => boolean,
): Promise<OpenRepositorySnapshot> {
  const refs: Array<{ name: string; target?: { oid?: string } }> = [];
  const prs: OpenPrNode[] = [];
  let defaultBranch: string | null = null;
  let refsComplete = false;
  let prsComplete = false;
  let openOk = false;

  const initial = await request<OpenSnapshot>([
    "api",
    "graphql",
    "-f",
    `query=${OPEN_QUERY}`,
    "-F",
    `owner=${owner}`,
    "-F",
    `name=${name}`,
  ]);
  const repository = initial?.data?.repository;
  if (!repository) {
    return { defaultBranch, refs, prs, refsComplete, prsComplete, openOk };
  }

  openOk = !!(repository.refs && repository.pullRequests);
  defaultBranch = repository.defaultBranchRef?.name ?? null;
  if (repository.refs) {
    refs.push(...(repository.refs.nodes ?? []));
    refsComplete = repository.refs.pageInfo?.hasNextPage === false;
  }
  if (repository.pullRequests) {
    prs.push(...(repository.pullRequests.nodes ?? []));
    prsComplete = repository.pullRequests.pageInfo?.hasNextPage === false;
  }

  let refsCursor = repository.refs?.pageInfo?.hasNextPage
    ? repository.refs.pageInfo.endCursor ?? undefined
    : undefined;
  for (let page = 1; page < MAX_OPEN_PAGES && refsCursor && canRequest(); page += 1) {
    const snapshot = await request<{
      data?: { repository?: { refs?: OpenRepository["refs"] } };
    }>([
      "api",
      "graphql",
      "-f",
      `query=${OPEN_REFS_PAGE_QUERY}`,
      "-F",
      `owner=${owner}`,
      "-F",
      `name=${name}`,
      "-F",
      `after=${refsCursor}`,
    ]);
    const refPage = snapshot?.data?.repository?.refs;
    if (!refPage) break;
    refs.push(...(refPage.nodes ?? []));
    refsComplete = refPage.pageInfo?.hasNextPage === false;
    refsCursor = refsComplete ? undefined : refPage.pageInfo?.endCursor ?? undefined;
  }

  let prsCursor = repository.pullRequests?.pageInfo?.hasNextPage
    ? repository.pullRequests.pageInfo.endCursor ?? undefined
    : undefined;
  for (let page = 1; page < MAX_OPEN_PAGES && prsCursor && canRequest(); page += 1) {
    const snapshot = await request<{
      data?: { repository?: { pullRequests?: OpenRepository["pullRequests"] } };
    }>([
      "api",
      "graphql",
      "-f",
      `query=${OPEN_PRS_PAGE_QUERY}`,
      "-F",
      `owner=${owner}`,
      "-F",
      `name=${name}`,
      "-F",
      `after=${prsCursor}`,
    ]);
    const prPage = snapshot?.data?.repository?.pullRequests;
    if (!prPage) break;
    prs.push(...(prPage.nodes ?? []));
    prsComplete = prPage.pageInfo?.hasNextPage === false;
    prsCursor = prsComplete ? undefined : prPage.pageInfo?.endCursor ?? undefined;
  }

  return { defaultBranch, refs, prs, refsComplete, prsComplete, openOk };
}

export function createGhciAdapter(opts: GhciAdapterOptions = {}): Adapter {
  const id = opts.id ?? "ghci";
  const interval = opts.interval ?? 60_000;
  const http = opts.runGh ? null : createGhHttp();
  const runGh = opts.runGh ?? http!.run;
  const now = opts.now ?? Date.now;
  const runsPerRepo = opts.runsPerRepo ?? DEFAULT_RUNS_PER_REPO;
  const configuredRepos = opts.repos ? [...opts.repos] : DEFAULT_REPOS;
  const persistRunHistory = opts.persistRunHistory ?? !opts.runGh;
  const cache = new Map<string, RepoPanel>();
  const mergedPrCache = new Map<string, MergedPrNode[]>();
  const amplificationHistory = new Map<string, Map<number, AmplificationEntry>>();
  const cadence = new Map<string, Cadence>();
  const logCache = new Map<number, { infraFlag: boolean; matchedSignature: string | null }>();
  let baseCursor = 0;
  let jobCursor = 0;
  let logCursor = 0;

  async function poll(): Promise<AdapterResult> {
    const items: Item[] = [];
    const completeScopes = new Set<string>();
    const nowMs = now();
    const ts = new Date(nowMs).toISOString();
    const runHistoryStore: RunHistoryStore = opts.runHistoryStore
      ?? (persistRunHistory ? readRunHistory() : {});
    if (configuredRepos.length === 0) {
      baseCursor = 0;
      return {
        items,
        panels: [{ id: "ci", ts, data: { repos: [], runners: [], runnersComplete: true, oldestQueuedAgeH: null } }],
        completeScopes: [],
      };
    }

    const admittedCount = Math.min(MAX_BASE_REPOS, configuredRepos.length);
    const admitted = Array.from(
      { length: admittedCount },
      (_, offset) => configuredRepos[(baseCursor + offset) % configuredRepos.length]!,
    );
    baseCursor = (baseCursor + admittedCount) % configuredRepos.length;
    const admittedSet = new Set(admitted);
    const rows = new Map<string, RepoPanel>();
    for (const repo of configuredRepos) {
      const cached = cache.get(repo);
      if (admittedSet.has(repo)) {
        const row = cloneRow(cached ?? emptyRow(repo));
        row.degraded = [];
        rows.set(repo, row);
      } else {
        rows.set(repo, budgetRow(cached ?? emptyRow(repo)));
      }
    }

    let used = 0;
    let reserved = admitted.reduce((total, repo) => {
      const state = cadence.get(repo) ?? { counter: 0, due: false };
      if (state.counter >= 9) state.due = true;
      cadence.set(repo, state);
      return total + 2 + (state.due ? 1 : 0);
    }, 0);
    const request = async <T>(args: string[]): Promise<T | null> => {
      if (used + reserved >= MAX_CALLS_PER_POLL) return null;
      used += 1;
      try {
        return JSON.parse(await runGh(args)) as T;
      } catch {
        return null;
      }
    };
    const requestLogTail = async (args: string[]): Promise<Uint8Array | null> => {
      if (used + reserved >= MAX_CALLS_PER_POLL) return null;
      used += 1;
      try {
        if (opts.runGh) return tailBytes(await runGh(args), LOG_TAIL_BYTES);
        return await http!.logTail(args, LOG_TAIL_BYTES);
      } catch {
        return null;
      }
    };

    const pollRepos: PollRepo[] = admitted.map((repo) => {
      const [owner = "", name = ""] = repo.split("/");
      const state = cadence.get(repo)!;
      return {
        repo,
        owner,
        name,
        row: rows.get(repo)!,
        historyOk: false,
        queuedOk: false,
        inProgressOk: false,
        runnersOk: false,
        openOk: false,
        due: state.due,
        defaultBranch: null,
        refs: [],
        prs: [],
        identities: [],
        exactRuns: new Map(),
      };
    });

    for (const state of pollRepos) {
      const base = `/repos/${state.owner}/${state.name}/actions`;
      const history = await request<GhRunsResponse>([
        "api",
        `${base}/runs?per_page=100&exclude_pull_requests=false`,
      ]);
      if (validRunResponse(history)) {
        state.historyOk = true;
        syncRunHistory(state.row, runHistoryStore, state.repo, history.workflow_runs, ts, runsPerRepo);
        if (state.row.historyComplete) {
          collectFailureItems(items, id, state.repo, storedRuns(runHistoryStore, state.repo, HISTORY_PAGE_SIZE));
          completeScopes.add(`run-failure:${state.repo}`);
        }
      } else {
        applyStoredRuns(state.row, runHistoryStore, state.repo, runsPerRepo);
      }

      const queued = await request<GhRunsResponse>([
        "api",
        `${base}/runs?status=queued&per_page=1&exclude_pull_requests=false`,
      ]);
      const inProgress = await request<GhRunsResponse>([
        "api",
        `${base}/runs?status=in_progress&per_page=1&exclude_pull_requests=false`,
      ]);
      state.queuedOk = validCount(queued?.total_count);
      state.inProgressOk = validCount(inProgress?.total_count);
      state.row.queueComplete = state.queuedOk && state.inProgressOk;
      state.row.queueDepth = state.row.queueComplete
        ? queued!.total_count + inProgress!.total_count
        : null;

      const runners = used + reserved < MAX_CALLS_PER_POLL
        ? await (async () => {
          used += 1;
          return fetchRepoRunners(runGh, state.owner, state.name);
        })()
        : null;
      if (runners === "not-found") {
        state.runnersOk = true;
        applyRunnerSnapshot(state.row, { total_count: 0, runners: [] });
      } else if (runners) {
        state.runnersOk = true;
        applyRunnerSnapshot(state.row, runners);
      } else {
        state.row.runnersComplete = false;
        state.row.runnersBusy = null;
        state.row.runnersTotal = null;
      }
    }

    for (const state of pollRepos) {
      const open = await loadOpenRepository(
        request,
        state.owner,
        state.name,
        () => used + reserved < MAX_CALLS_PER_POLL,
      );
      state.defaultBranch = open.defaultBranch;
      state.refs = open.refs;
      state.prs = open.prs;
      state.openOk = open.openOk;
      if (open.openOk) {
        state.row.refsComplete = open.refsComplete;
        if (!open.refsComplete) addDegraded(state.row, "pagination");
        state.row.prsComplete = open.prsComplete;
        if (!open.prsComplete) addDegraded(state.row, "pagination");
        const memberToTrain = trainMembership(open.prs);
        state.row.prs = open.prs.map((pr) => panelPr(state.repo, pr, state.defaultBranch, memberToTrain, nowMs));
        state.row.prQueueDepth = open.prsComplete ? open.prs.length : null;
        state.row.oldestQueuedAgeH = open.prsComplete ? oldestAgeHours(open.prs, nowMs) : null;
        if (state.row.prs.some((pr) => pr.checksComplete === false)) addDegraded(state.row, "pagination");
      } else {
        state.row.refsComplete = false;
        state.row.prsComplete = false;
        state.row.prQueueDepth = null;
        state.row.oldestQueuedAgeH = null;
        for (const pr of state.row.prs) pr.checksComplete = false;
      }
      state.identities = trainIdentities(state.refs, state.prs, mergedPrCache.get(state.repo) ?? []);
      if (state.openOk) reserved -= 2 - Math.min(2, exactIdentities(state.identities).length);
    }

    for (const state of pollRepos) {
      const fixedOk = state.historyOk && state.queuedOk && state.inProgressOk && state.runnersOk && state.openOk;
      const cadenceState = cadence.get(state.repo)!;
      if (!fixedOk) continue;
      cadenceState.counter = Math.min(10, cadenceState.counter + 1);
      if (!state.due) continue;
      const date = new Date(nowMs);
      const today = utcDate(date);
      const firstDay = utcDate(new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() - 6)));
      const searchQuery = `repo:${state.repo} is:pr is:merged merged:${firstDay}..${today}`;
      reserved -= 1;
      const merged = await request<MergedSnapshot>([
        "api",
        "graphql",
        "-f",
        `query=${mergedQuery(searchQuery)}`,
      ]);
      const search = merged?.data?.search;
      if (search && validCount(search.issueCount) && Array.isArray(search.nodes)) {
        const timestamped = search.nodes.filter(hasValidMergedTimestamps);
        if (
          search.pageInfo?.hasNextPage !== false
          || search.issueCount > search.nodes.length
          || timestamped.length !== search.nodes.length
        ) {
          addDegraded(state.row, "pagination");
          clearMergedMetrics(state.row);
        } else {
          mergedPrCache.set(state.repo, search.nodes.filter(validMergedPr));
          setMergedMetrics(state.row, timestamped, nowMs, ts);
        }
        cadenceState.counter = 0;
        cadenceState.due = false;
      } else {
        addDegraded(state.row, "pagination");
        clearMergedMetrics(state.row);
      }
    }

    for (const state of pollRepos) {
      state.identities = trainIdentities(state.refs, state.prs, mergedPrCache.get(state.repo) ?? []);
      const exact = exactIdentities(state.identities);
      if (state.identities.filter((identity) => identity.openPr !== null).length > 2) {
        addDegraded(state.row, "jobs");
      }
      state.row.trainRunsComplete = state.openOk && exact.length <= 2;
      for (const identity of state.openOk ? exact.slice(0, 2) : []) {
        reserved -= 1;
        const response = await request<GhRunsResponse>([
          "api",
          `/repos/${state.owner}/${state.name}/actions/runs?branch=${encodeURIComponent(identity.branch)}&head_sha=${encodeURIComponent(identity.oid)}&per_page=100&exclude_pull_requests=false`,
        ]);
        if (!validRunResponse(response)) {
          state.row.trainRunsComplete = false;
          continue;
        }
        if (
          response.total_count !== response.workflow_runs.length
          || response.workflow_runs.some((run) => run.head_sha !== identity.oid)
        ) {
          state.row.trainRunsComplete = false;
          addDegraded(state.row, "pagination");
        }
        state.exactRuns.set(identity.branch, response.workflow_runs.filter((run) => run.head_sha === identity.oid));
      }
      state.row.trainsComplete = state.row.refsComplete && state.row.prsComplete && state.row.trainRunsComplete;
      if (state.openOk) state.row.trains = buildTrains(state);
      else suppressTrains(state.row, trainStateGap(state.row) ?? "train refs incomplete");
    }

    const jobCandidates = fairCandidates(pollRepos, configuredRepos, jobCursor, (state) =>
      state.row.trainsComplete
        ? state.row.trains.filter((train) => train.prNumber !== null && train.gateRun && train.state !== "merged").slice(0, 2)
        : [],
    );
    for (const candidate of jobCandidates) {
      if (used + reserved >= MAX_CALLS_PER_POLL) break;
      jobCursor = successorIndex(configuredRepos, candidate.state.repo);
      const jobs = await request<GhJobsResponse>([
        "api",
        `/repos/${candidate.state.owner}/${candidate.state.name}/actions/runs/${candidate.value.gateRun.id}/jobs?filter=latest&per_page=100`,
      ]);
      if (!validJobsResponse(jobs) || jobs.total_count !== jobs.jobs.length) {
        candidate.value.gateRun.jobsComplete = false;
        addDegraded(candidate.state.row, jobs ? "pagination" : "jobs");
        continue;
      }
      candidate.value.gateRun.jobsComplete = true;
      candidate.value.gateRun.jobs = jobs.jobs.map((job) => panelJob(job, logCache));
      const run = candidate.state.exactRuns.get(candidate.value.branch)
        ?.find((entry) => entry.id === candidate.value.gateRun.id);
      if (run) recordAmplification(amplificationHistory, candidate.state.repo, run, jobs.jobs);
    }

    const logCandidates = fairCandidates(pollRepos, configuredRepos, logCursor, (state) =>
      state.row.trains.flatMap((train) =>
        (train.gateRun?.jobs ?? []).filter(
          (job: Record<string, any>) => job.conclusion === "failure" && !logCache.has(job.id),
        ),
      ),
    );
    for (const candidate of logCandidates) {
      const cachedLog = logCache.get(candidate.value.id);
      if (cachedLog) {
        candidate.value.infraFlag = cachedLog.infraFlag;
        continue;
      }
      if (used + reserved >= MAX_CALLS_PER_POLL) break;
      logCursor = successorIndex(configuredRepos, candidate.state.repo);
      const log = await requestLogTail([
        "api",
        `/repos/${candidate.state.owner}/${candidate.state.name}/actions/jobs/${candidate.value.id}/logs`,
      ]);
      if (log === null) continue;
      const matchedSignature = new TextDecoder().decode(log).match(INFRA_PATTERN)?.[0] ?? null;
      const classified = { infraFlag: matchedSignature !== null, matchedSignature };
      logCache.set(candidate.value.id, classified);
      candidate.value.infraFlag = classified.infraFlag;
    }

    for (const state of pollRepos) {
      if (
        state.row.trainsComplete
        && state.row.trains.some((train) => train.gateRun?.jobsComplete === false)
      ) {
        addDegraded(state.row, "jobs");
      }
      finalizeTrains(state.row, logCache);
      joinRunnerOccupancy(state.row);
      collectTrainInboxItems(items, completeScopes, id, state, nowMs, ts);
    }

    for (const [repo, row] of rows) {
      if (row.pollDeferred) applyStoredRuns(row, runHistoryStore, repo, runsPerRepo);
      row.lag.gates = amplificationGates(amplificationHistory.get(repo));
    }

    for (const state of pollRepos) cache.set(state.repo, cloneRow(state.row));
    if (persistRunHistory && !opts.runHistoryStore) writeRunHistory(runHistoryStore);
    const repoPanels = configuredRepos.map((repo) => rows.get(repo)!);
    const fleet = await buildCiRunnerFleet(runGh, repoPanels, () => used + reserved < MAX_CALLS_PER_POLL, (spent) => {
      used += spent;
    });
    const panels: Panel[] = [{
      id: "ci",
      ts,
      data: {
        repos: repoPanels,
        runners: fleet.runners,
        runnersComplete: fleet.runnersComplete,
        oldestQueuedAgeH: fleet.oldestQueuedAgeH,
      } satisfies CiPanelData,
    }];
    return { items, panels, completeScopes: [...completeScopes] };
  }

  return { id, interval, poll };
}

function validCount(value: unknown): value is number {
  return Number.isSafeInteger(value) && (value as number) >= 0;
}

function validRunResponse(value: GhRunsResponse | null | undefined): value is GhRunsResponse {
  return !!value && validCount(value.total_count) && Array.isArray(value.workflow_runs);
}

function validRunnerResponse(value: GhRunnersResponse | null): value is GhRunnersResponse {
  return !!value && validCount(value.total_count) && Array.isArray(value.runners);
}

function applyRunnerSnapshot(row: RepoPanel, runners: GhRunnersResponse): void {
  row.runners = runners.runners.map((runner) => ({
    id: runner.id,
    name: runner.name,
    status: runner.status,
    busy: runner.busy,
    currentJob: null,
    currentRepo: null,
    currentKnown: !runner.busy,
  }));
  row.runnersComplete = runners.total_count === runners.runners.length;
  const online = runners.runners.filter((runner) => runner.status === "online");
  row.runnersBusy = row.runnersComplete ? online.filter((runner) => runner.busy).length : null;
  row.runnersTotal = row.runnersComplete ? online.length : null;
  if (!row.runnersComplete) addDegraded(row, "pagination");
}

async function fetchRepoRunners(
  runGh: GhRunner,
  owner: string,
  name: string,
): Promise<GhRunnersResponse | "not-found" | null> {
  try {
    const runners = JSON.parse(
      await runGh(["api", `/repos/${owner}/${name}/actions/runners?per_page=100`]),
    ) as GhRunnersResponse;
    return validRunnerResponse(runners) ? runners : null;
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    if (/\b404\b|not found/i.test(message)) return "not-found";
    return null;
  }
}

async function fetchOrgRunners(
  runGh: GhRunner,
  owner: string,
): Promise<GhRunnersResponse | "not-found" | null> {
  try {
    const runners = JSON.parse(
      await runGh(["api", `/orgs/${owner}/actions/runners?per_page=100`]),
    ) as GhRunnersResponse;
    return validRunnerResponse(runners) ? runners : null;
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    if (/\b404\b|not found/i.test(message)) return "not-found";
    return null;
  }
}

function panelRunnerEntry(runner: GhRunnerEntry, scope: string): FleetRunner {
  return {
    id: runner.id,
    name: runner.name,
    status: runner.status,
    busy: runner.busy,
    scope,
    currentJob: null,
    currentRepo: null,
    currentKnown: !runner.busy,
  };
}

function joinFleetOccupancy(runners: FleetRunner[], rows: RepoPanel[]): void {
  const activeByRunner = new Map<number, { job: Record<string, any>; repo: string }>();
  for (const row of rows) {
    for (const train of row.trains) {
      for (const job of train.gateRun?.jobs ?? []) {
        if (job.status !== "in_progress" || job.runnerId === null) continue;
        const previous = activeByRunner.get(job.runnerId);
        if (
          !previous
          || (job.startedAt ?? "").localeCompare(previous.job.startedAt ?? "") > 0
          || ((job.startedAt ?? "") === (previous.job.startedAt ?? "") && job.id > previous.job.id)
        ) {
          activeByRunner.set(job.runnerId, { job, repo: row.repo });
        }
      }
    }
  }
  for (const runner of runners) {
    const active = activeByRunner.get(runner.id);
    runner.currentJob = active?.job.name ?? null;
    runner.currentRepo = active?.repo ?? null;
    runner.currentKnown = !!active || runner.busy === false;
  }
}

async function buildCiRunnerFleet(
  runGh: GhRunner,
  rows: RepoPanel[],
  canRequest: () => boolean,
  spend: (calls: number) => void,
): Promise<{ runners: FleetRunner[]; runnersComplete: boolean; oldestQueuedAgeH: number | null }> {
  const byId = new Map<number, FleetRunner>();
  let runnersComplete = true;

  for (const row of rows) {
    if (!row.runnersComplete) runnersComplete = false;
    for (const runner of row.runners) {
      const id = runner.id as number;
      if (!Number.isSafeInteger(id)) continue;
      byId.set(id, {
        id,
        name: String(runner.name),
        status: String(runner.status),
        busy: runner.busy === true,
        scope: row.repo,
        currentJob: (runner.currentJob as string | null) ?? null,
        currentRepo: (runner.currentRepo as string | null) ?? null,
        currentKnown: runner.currentKnown !== false,
      });
    }
  }

  const owners = [...new Set(rows.map((row) => row.repo.split("/")[0]!).filter(Boolean))];
  for (const owner of owners) {
    if (!canRequest()) {
      runnersComplete = false;
      break;
    }
    spend(1);
    const orgRunners = await fetchOrgRunners(runGh, owner);
    if (orgRunners === null) {
      runnersComplete = false;
      continue;
    }
    if (orgRunners === "not-found") continue;
    if (orgRunners.total_count !== orgRunners.runners.length) runnersComplete = false;
    for (const runner of orgRunners.runners) {
      if (byId.has(runner.id)) continue;
      byId.set(runner.id, panelRunnerEntry(runner, `org:${owner}`));
    }
  }

  const runners = [...byId.values()].sort((left, right) =>
    left.name.localeCompare(right.name) || left.id - right.id,
  );
  joinFleetOccupancy(runners, rows);

  const queuedAges = rows
    .map((row) => row.oldestQueuedAgeH)
    .filter((value): value is number => value !== null);
  const oldestQueuedAgeH = queuedAges.length === 0 ? null : Math.max(...queuedAges);

  return { runners, runnersComplete, oldestQueuedAgeH };
}


function validJobsResponse(value: GhJobsResponse | null): value is GhJobsResponse {
  return !!value && validCount(value.total_count) && Array.isArray(value.jobs);
}

function validMergedPr(value: unknown): value is MergedPrNode {
  if (!value || typeof value !== "object") return false;
  const pr = value as Record<string, unknown>;
  return validCount(pr.number)
    && typeof pr.body === "string"
    && typeof pr.url === "string"
    && typeof pr.headRefName === "string"
    && typeof pr.headRefOid === "string"
    && typeof pr.createdAt === "string"
    && typeof pr.mergedAt === "string";
}

function emptyRow(repo: string): RepoPanel {
  return {
    repo,
    queueDepth: null,
    runs: [],
    runners: [],
    prs: [],
    trains: [],
    degraded: [],
    queueComplete: false,
    historyComplete: false,
    trainRunsComplete: false,
    runnersComplete: false,
    refsComplete: false,
    prsComplete: false,
    trainsComplete: false,
    pollDeferred: false,
    prQueueDepth: null,
    oldestQueuedAgeH: null,
    runnersBusy: null,
    runnersTotal: null,
    lag: {
      landsToday: null,
      medianOpenToMergeH7d: null,
      gates: [],
      warmedAt: null,
    },
  };
}

function cloneRow(row: RepoPanel): RepoPanel {
  return structuredClone(row);
}

function budgetRow(row: RepoPanel): RepoPanel {
  const cached = cloneRow(row);
  cached.pollDeferred = true;
  return cached;
}

function suppressTrains(row: RepoPanel, gap: string): void {
  for (const train of row.trains) {
    train.state = null;
    train.stateGap = gap;
    train.actionEligible = false;
    train.gateRun = null;
    train.blockedBy = null;
    train.infraFlag = null;
  }
}

function addDegraded(row: RepoPanel, reason: "budget" | "pagination" | "jobs"): void {
  const order = ["budget", "pagination", "jobs"] as const;
  if (!row.degraded.includes(reason)) row.degraded.push(reason);
  row.degraded.sort((left, right) => order.indexOf(left) - order.indexOf(right));
}

function panelRun(run: GhRun): Record<string, unknown> {
  return {
    id: run.id,
    name: run.name,
    headBranch: run.head_branch,
    headSha: run.head_sha,
    status: run.status,
    conclusion: run.conclusion,
    htmlUrl: run.html_url,
    updatedAt: run.updated_at,
  };
}

function panelPr(
  repo: string,
  pr: OpenPrNode,
  defaultBranch: string | null,
  memberToTrain: Map<number, number>,
  nowMs: number,
): Record<string, unknown> {
  const rollup = pr.commits?.nodes?.[0]?.commit?.statusCheckRollup;
  const contexts = rollup?.contexts;
  const checksComplete = rollup == null || contexts?.pageInfo?.hasNextPage === false;
  return {
    repo,
    number: pr.number,
    title: pr.title,
    body: pr.body,
    url: pr.url,
    branch: pr.headRefName,
    headRefOid: pr.headRefOid,
    baseRefName: pr.baseRefName,
    createdAt: pr.createdAt,
    mergeable: pr.mergeable,
    mergeStateStatus: pr.mergeStateStatus,
    ageH: ageHours(pr.createdAt, nowMs),
    staleVsMain: staleVsMain(pr, defaultBranch),
    trainPr: memberToTrain.get(pr.number) ?? null,
    checksComplete,
    checks: (contexts?.nodes ?? []).map((check) => ({
      name: check.__typename === "CheckRun" ? check.name : check.context,
      bucket: checkBucket(check),
    })),
  };
}

function checkBucket(check: CheckNode): "pass" | "fail" | "pending" | "skipping" | "cancel" {
  if (check.__typename === "CheckRun" && check.status !== "COMPLETED") return "pending";
  const result = check.__typename === "CheckRun" ? check.conclusion : check.state;
  if (result === "SUCCESS" || result === "NEUTRAL") return "pass";
  if (result === "SKIPPED") return "skipping";
  if (result === "CANCELLED" || result === "STALE") return "cancel";
  if (["FAILURE", "ERROR", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE"].includes(result ?? "")) return "fail";
  return "pending";
}

function staleVsMain(pr: OpenPrNode, defaultBranch: string | null): boolean | null {
  if (!defaultBranch || !pr.baseRefName || !pr.mergeStateStatus || pr.baseRefName !== defaultBranch || pr.mergeStateStatus === "UNKNOWN") return null;
  return pr.mergeStateStatus === "BEHIND";
}

function ageHours(timestamp: string, nowMs: number): number {
  const parsed = Date.parse(timestamp);
  return Number.isFinite(parsed) ? Math.max(0, (nowMs - parsed) / 3_600_000) : 0;
}

function oldestAgeHours(prs: OpenPrNode[], nowMs: number): number | null {
  if (prs.length === 0) return 0;
  const timestamps = prs.map((pr) => Date.parse(pr.createdAt));
  if (timestamps.some((timestamp) => !Number.isFinite(timestamp))) return null;
  return Math.max(0, (nowMs - Math.min(...timestamps)) / 3_600_000);
}

function hasValidMergedTimestamps(value: unknown): value is MergedTimestampNode {
  if (!value || typeof value !== "object") return false;
  const pr = value as Record<string, unknown>;
  return parseTimestamp(pr.createdAt) !== null && parseTimestamp(pr.mergedAt) !== null;
}

function clearMergedMetrics(row: RepoPanel): void {
  row.lag.landsToday = null;
  row.lag.medianOpenToMergeH7d = null;
}

function setMergedMetrics(row: RepoPanel, prs: MergedTimestampNode[], nowMs: number, warmedAt: string): void {
  const now = new Date(nowMs);
  const todayStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
  const sevenDayStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - 6);
  const mergedTimes = prs.map((pr) => ({ created: parseTimestamp(pr.createdAt)!, merged: parseTimestamp(pr.mergedAt)! }));
  row.lag.landsToday = mergedTimes.filter(({ merged }) => merged >= todayStart && merged <= nowMs).length;
  const durations = mergedTimes
    .filter(({ created, merged }) => merged >= sevenDayStart && merged <= nowMs && created <= merged)
    .map(({ created, merged }) => (merged - created) / 3_600_000)
    .sort((left, right) => left - right);
  row.lag.medianOpenToMergeH7d = durations.length >= 3 ? median(durations) : null;
  row.lag.warmedAt = warmedAt;
}

function median(sorted: number[]): number {
  const middle = Math.floor(sorted.length / 2);
  return sorted.length % 2 === 1
    ? sorted[middle]!
    : (sorted[middle - 1]! + sorted[middle]!) / 2;
}

function recordAmplification(
  history: Map<string, Map<number, AmplificationEntry>>,
  repo: string,
  run: GhRun,
  jobs: GhJob[],
): void {
  if (run.status !== "completed") return;
  const createdAtMs = parseTimestamp(run.created_at);
  const completedAtMs = parseTimestamp(run.updated_at);
  if (createdAtMs === null || completedAtMs === null || createdAtMs > completedAtMs) return;
  const jobTimes = jobs.map((job) => ({ started: parseTimestamp(job.started_at), completed: parseTimestamp(job.completed_at) }));
  if (jobTimes.some(({ started, completed }) => started === null || completed === null || started > completed)) return;
  const ring = history.get(repo) ?? new Map<number, AmplificationEntry>();
  ring.set(run.id, {
    runId: run.id,
    computeMin: jobTimes.reduce((total, job) => total + job.completed! - job.started!, 0) / 60_000,
    wallMin: (completedAtMs - createdAtMs) / 60_000,
    completedAtMs,
  });
  const retained = [...ring.values()].sort(compareAmplification).slice(0, 50);
  history.set(repo, new Map(retained.map((entry) => [entry.runId, entry])));
}

function parseTimestamp(value: unknown): number | null {
  if (typeof value !== "string") return null;
  const parsed = Date.parse(value);
  return Number.isFinite(parsed) ? parsed : null;
}

function compareAmplification(left: AmplificationEntry, right: AmplificationEntry): number {
  return right.completedAtMs - left.completedAtMs || right.runId - left.runId;
}

function amplificationGates(ring: Map<number, AmplificationEntry> | undefined): Array<{ runId: number; computeMin: number; wallMin: number }> {
  return [...(ring?.values() ?? [])]
    .sort(compareAmplification)
    .map(({ runId, computeMin, wallMin }) => ({ runId, computeMin, wallMin }));
}

function parseTrainMembers(body: string): number[] {
  const members: number[] = [];
  const seen = new Set<number>();
  const addMatches = (text: string, pattern: RegExp) => {
    for (const match of text.matchAll(pattern)) {
      const number = Number(match[1]);
      if (!seen.has(number)) {
        seen.add(number);
        members.push(number);
      }
    }
  };
  const trailer = body.match(/^Train-Members:\s*(.*)$/m)?.[1] ?? "";
  addMatches(trailer, /#(\d+)/g);
  addMatches(body, /^-\s+#(\d+)\s/mg);
  return members;
}

function trainMembership(prs: OpenPrNode[]): Map<number, number> {
  const membership = new Map<number, number>();
  for (const pr of prs) {
    if (!pr.headRefName.startsWith(TRAIN_PREFIX)) continue;
    for (const member of parseTrainMembers(pr.body)) {
      const current = membership.get(member);
      if (current === undefined || pr.number > current) membership.set(member, pr.number);
    }
  }
  return membership;
}

function trainIdentities(
  refs: Array<{ name: string; target?: { oid?: string } }>,
  prs: OpenPrNode[],
  mergedPrs: MergedPrNode[],
): TrainIdentity[] {
  const refsByBranch = new Map<string, string>();
  const openByBranch = new Map<string, OpenPrNode>();
  const mergedByBranch = new Map<string, MergedPrNode>();
  for (const ref of refs) {
    const oid = ref.target?.oid;
    if (ref.name.startsWith(TRAIN_PREFIX) && oid) refsByBranch.set(ref.name, oid);
  }
  for (const pr of prs) {
    if (!pr.headRefName.startsWith(TRAIN_PREFIX) || !pr.headRefOid) continue;
    openByBranch.set(pr.headRefName, pr);
  }
  for (const pr of mergedPrs) {
    if (pr.headRefName.startsWith(TRAIN_PREFIX) && pr.headRefOid) mergedByBranch.set(pr.headRefName, pr);
  }
  const branches = new Set([...refsByBranch.keys(), ...openByBranch.keys(), ...mergedByBranch.keys()]);
  return [...branches].map((branch) => {
    const refOid = refsByBranch.get(branch);
    const open = openByBranch.get(branch) ?? null;
    const merged = mergedByBranch.get(branch) ?? null;
    const oid = refOid ?? open?.headRefOid ?? merged?.headRefOid ?? "";
    return {
      branch,
      oid,
      ref: refOid !== undefined,
      openPr: open?.headRefOid === oid ? open : null,
      mergedPr: open?.headRefOid === oid ? null : merged?.headRefOid === oid ? merged : null,
    };
  }).sort((left, right) => {
    const leftNumber = trainNumber(left.branch);
    const rightNumber = trainNumber(right.branch);
    if (leftNumber !== rightNumber) {
      if (leftNumber === null) return 1;
      if (rightNumber === null) return -1;
      return rightNumber - leftNumber;
    }
    const createdOrder = compareNullableDescending(identityPr(left)?.createdAt, identityPr(right)?.createdAt);
    if (createdOrder !== 0) return createdOrder;
    const prOrder = compareNullableDescending(identityPr(left)?.number, identityPr(right)?.number);
    if (prOrder !== 0) return prOrder;
    return left.branch.localeCompare(right.branch);
  });
}

function identityPr(identity: TrainIdentity): OpenPrNode | MergedPrNode | null {
  return identity.openPr ?? identity.mergedPr;
}

function exactIdentities(identities: TrainIdentity[]): TrainIdentity[] {
  return identities.filter((identity) => identity.mergedPr === null);
}

function compareNullableDescending(left: string | number | undefined, right: string | number | undefined): number {
  if (left === undefined) return right === undefined ? 0 : 1;
  if (right === undefined) return -1;
  if (left < right) return 1;
  if (left > right) return -1;
  return 0;
}

function trainNumber(branch: string): number | null {
  const match = branch.match(/batch-train-(\d+)$/);
  return match ? Number(match[1]) : null;
}

function compareTrains(left: Record<string, any>, right: Record<string, any>): number {
  const leftNumber = trainNumber(left.branch);
  const rightNumber = trainNumber(right.branch);
  if (leftNumber !== rightNumber) {
    if (leftNumber === null) return 1;
    if (rightNumber === null) return -1;
    return rightNumber - leftNumber;
  }
  const createdOrder = compareNullableDescending(left.createdAt ?? undefined, right.createdAt ?? undefined);
  if (createdOrder !== 0) return createdOrder;
  const prOrder = compareNullableDescending(left.prNumber ?? undefined, right.prNumber ?? undefined);
  if (prOrder !== 0) return prOrder;
  return left.branch.localeCompare(right.branch);
}

function trainStateGap(row: RepoPanel): string | null {
  if (!row.refsComplete) return "train refs incomplete";
  if (!row.prsComplete) return "PRs incomplete";
  if (!row.trainRunsComplete) return "train runs incomplete";
  return null;
}

function buildTrains(state: PollRepo): Array<Record<string, any>> {
  const previousByBranch = new Map(state.row.trains.map((train) => [train.branch, train]));
  const gap = trainStateGap(state.row);
  const output: Array<Record<string, any>> = state.identities.map((identity) => {
    const runs = state.exactRuns.get(identity.branch) ?? [];
    const latest = [...runs].sort((left, right) =>
      right.created_at.localeCompare(left.created_at) || right.run_attempt - left.run_attempt || right.id - left.id,
    )[0];
    let lifecycle: string | null = null;
    let stateGap: string | null = gap;
    if (!gap) {
      if (identity.openPr) {
        if (latest && latest.run_attempt > 1 && latest.status === "in_progress") lifecycle = "rerunning";
        else if (latest?.status === "queued" || latest?.status === "in_progress") lifecycle = "gating";
        else if (latest?.status === "completed" && latest.conclusion === "failure") lifecycle = "failed";
        else if (latest?.status === "completed" && latest.conclusion === "success") lifecycle = "green";
        else stateGap = latest ? "unsupported train run state" : "latest train run unavailable";
      } else if (identity.mergedPr) lifecycle = "merged";
      else if (identity.ref) lifecycle = "building";
      else stateGap = "train lifecycle unavailable";
    }
    const previous = previousByBranch.get(identity.branch);
    const previousGateRun = previous && latest && previous.gateRun?.id === latest.id ? previous.gateRun : null;
    const pr = identityPr(identity);
    return {
      repo: state.repo,
      branch: identity.branch,
      oid: identity.oid,
      prNumber: pr?.number ?? null,
      prUrl: pr?.url ?? null,
      createdAt: pr?.createdAt ?? null,
      members: parseTrainMembers(pr?.body ?? ""),
      state: lifecycle,
      stateGap,
      actionEligible: gap === null && identity.openPr !== null && lifecycle !== null,
      gateRun: gap === null && identity.openPr && latest
        ? {
            id: latest.id,
            attempt: latest.run_attempt,
            status: latest.status,
            conclusion: latest.conclusion,
            jobsComplete: false,
            jobs: structuredClone(previousGateRun?.jobs ?? []),
          }
        : null,
      blockedBy: null,
      infraFlag: null,
    };
  });
  if (gap && (!state.row.refsComplete || !state.row.prsComplete)) {
    for (const previous of previousByBranch.values()) {
      if (output.some((train) => train.branch === previous.branch)) continue;
      const retained = structuredClone(previous);
      retained.state = null;
      retained.stateGap = gap;
      retained.gateRun = null;
      retained.actionEligible = false;
      retained.blockedBy = null;
      retained.infraFlag = null;
      output.push(retained);
    }
  }
  return output.sort(compareTrains);
}

function panelJob(
  job: GhJob,
  logCache: Map<number, { infraFlag: boolean; matchedSignature: string | null }>,
): Record<string, unknown> {
  return {
    id: job.id,
    name: job.name,
    status: job.status,
    conclusion: job.conclusion,
    runnerId: job.runner_id,
    runner: job.runner_name,
    startedAt: job.started_at,
    completedAt: job.completed_at,
    infraFlag: job.conclusion === "failure" ? logCache.get(job.id)?.infraFlag ?? null : false,
  };
}

function finalizeTrains(
  row: RepoPanel,
  logCache: Map<number, { infraFlag: boolean; matchedSignature: string | null }>,
): void {
  for (const train of row.trains) {
    const jobs = train.gateRun?.jobs as Array<Record<string, any>> | undefined;
    if (!train.gateRun || !jobs) {
      train.infraFlag = null;
      train.blockedBy = null;
      continue;
    }
    for (const job of jobs) {
      if (job.conclusion === "failure") job.infraFlag = logCache.get(job.id)?.infraFlag ?? null;
    }
    const failedJobs = jobs
      .filter((job) => job.conclusion === "failure")
      .sort(compareJobsByStartThenId);
    const classificationsComplete = train.gateRun.jobsComplete
      && failedJobs.every((job) => job.infraFlag !== null);
    train.infraFlag = failedJobs.some((job) => job.infraFlag === true)
      ? true
      : classificationsComplete ? false : null;

    if (train.state === "failed") {
      const failed = failedJobs[0];
      if (!failed) {
        train.blockedBy = null;
      } else {
        const signature = logCache.get(failed.id)?.matchedSignature;
        train.blockedBy = failed.infraFlag && signature
          ? `${failed.name} — ${signature} (infra)`
          : `${failed.name} — ${failed.conclusion}`;
      }
    } else if (train.state === "gating" || train.state === "rerunning") {
      const active = jobs
        .filter((job) => job.status === "in_progress" || job.status === "queued")
        .sort((left, right) => {
          const statusOrder = Number(right.status === "in_progress") - Number(left.status === "in_progress");
          return statusOrder || compareJobsByStartThenId(left, right);
        })[0];
      train.blockedBy = active?.name ?? null;
    } else {
      train.blockedBy = null;
    }
  }
}

function compareJobsByStartThenId(left: Record<string, any>, right: Record<string, any>): number {
  if (left.startedAt === null) return right.startedAt === null ? left.id - right.id : 1;
  if (right.startedAt === null) return -1;
  const leftStart = left.startedAt;
  const rightStart = right.startedAt;
  return leftStart.localeCompare(rightStart) || left.id - right.id;
}

function joinRunnerOccupancy(row: RepoPanel): void {
  const activeByRunner = new Map<number, Record<string, any>>();
  for (const train of row.trains) {
    for (const job of train.gateRun?.jobs ?? []) {
      if (job.status !== "in_progress" || job.runnerId === null) continue;
      const previous = activeByRunner.get(job.runnerId);
      if (
        !previous
        || (job.startedAt ?? "").localeCompare(previous.startedAt ?? "") > 0
        || ((job.startedAt ?? "") === (previous.startedAt ?? "") && job.id > previous.id)
      ) {
        activeByRunner.set(job.runnerId, job);
      }
    }
  }
  for (const runner of row.runners) {
    const job = activeByRunner.get(runner.id as number);
    runner.currentJob = job?.name ?? null;
    runner.currentRepo = job ? row.repo : null;
    runner.currentKnown = !!job || runner.busy === false;
  }
}

function tailBytes(value: string, maxBytes: number): Uint8Array {
  const bytes = new TextEncoder().encode(value);
  return bytes.length <= maxBytes ? bytes : bytes.slice(bytes.length - maxBytes);
}

function collectFailureItems(items: Item[], source: string, repo: string, runs: GhRun[]): void {
  const seenWorkflowKeys = new Set<string>();
  for (const run of runs) {
    if (run.status !== "completed") continue;
    const workflowKey = `${repo}:${run.workflow_id}:${run.head_branch}`;
    if (seenWorkflowKeys.has(workflowKey)) continue;
    seenWorkflowKeys.add(workflowKey);
    if (run.conclusion === "failure") items.push(failureItem(source, repo, run));
  }
}

function failureItem(source: string, repo: string, run: GhRun): Item {
  return {
    id: `ghci:${repo}:${run.id}:${run.run_attempt}`,
    source,
    project: repo,
    reconciliationScope: `run-failure:${repo}`,
    severity: "act",
    kind: "ci",
    title: `${run.name} failed on ${run.head_branch}`,
    detail: `${run.display_title} — run #${run.run_number} attempt ${run.run_attempt}: ${run.html_url}`,
    ts: run.updated_at,
    actions: [{ verb: "open", args: { url: run.html_url }, label: "Open run", recommended: true }],
  };
}

function collectTrainInboxItems(
  items: Item[],
  completeScopes: Set<string>,
  source: string,
  state: PollRepo,
  nowMs: number,
  ts: string,
): void {
  const infraScope = `train:${state.repo}:infra-failed`;
  const greenScope = `train:${state.repo}:green-unmerged`;
  const queueScope = `train:${state.repo}:queue-no-train`;
  const trainsKnown = state.row.refsComplete
    && state.row.trainsComplete
    && state.row.trains.every((train) => train.state !== null);
  if (!trainsKnown) return;

  const failedTrains = state.row.trains.filter((train) => train.state === "failed");
  const infraComplete = failedTrains.every((train) =>
    train.gateRun?.jobsComplete === true && train.infraFlag !== null
  );
  if (infraComplete) {
    completeScopes.add(infraScope);
    for (const train of failedTrains) {
      if (train.infraFlag !== true || !train.gateRun || train.prNumber === null) continue;
      const run = trainRun(state, train.branch, train.gateRun.id);
      items.push({
        id: `ghci:train:${state.repo}:${train.prNumber}:infra-failed`,
        source,
        project: state.repo,
        reconciliationScope: infraScope,
        severity: "act",
        kind: "ci",
        title: `Train #${train.prNumber} failed on infrastructure`,
        detail: train.blockedBy ?? `Gate run ${train.gateRun.id} has an infrastructure failure`,
        ts: run?.updated_at ?? ts,
        actions: [{
          verb: "ci.rerunFailed",
          args: { repo: state.repo, runId: String(train.gateRun.id) },
          label: "Rerun failed jobs",
          recommended: true,
        }],
      });
    }
  }

  const greenTrains = state.row.trains.filter((train) => train.state === "green");
  const completed = greenTrains.map((train) => {
    const run = train.gateRun ? trainRun(state, train.branch, train.gateRun.id) : undefined;
    return { train, completedAt: parseTimestamp(run?.updated_at) };
  });
  if (completed.every(({ completedAt }) => completedAt !== null)) {
    completeScopes.add(greenScope);
    for (const { train, completedAt } of completed) {
      if (
        completedAt === null
        || nowMs - completedAt <= 10 * 60_000
        || train.prNumber === null
        || !train.prUrl
      ) continue;
      items.push({
        id: `ghci:train:${state.repo}:${train.prNumber}:green-unmerged`,
        source,
        project: state.repo,
        reconciliationScope: greenScope,
        severity: "act",
        kind: "ci",
        title: `Train #${train.prNumber} is green but unmerged`,
        detail: "Gate completed more than 10 minutes ago",
        ts: new Date(completedAt).toISOString(),
        actions: [{
          verb: "open",
          args: { url: train.prUrl },
          label: "Open pull request",
          recommended: true,
        }],
      });
    }
  }

  if (state.row.queueComplete) {
    completeScopes.add(queueScope);
    const activeTrain = state.row.trains.some(
      (train) => train.state === "gating" || train.state === "rerunning",
    );
    if ((state.row.prQueueDepth ?? 0) >= 4 && !activeTrain) {
      items.push({
        id: `ghci:train:${state.repo}:queue-no-train`,
        source,
        project: state.repo,
        reconciliationScope: queueScope,
        severity: "warn",
        kind: "ci",
        title: "Queue building with no active train",
        detail: `${state.row.prQueueDepth} open pull requests`,
        ts,
        actions: [],
      });
    }
  }
}

function trainRun(state: PollRepo, branch: string, runId: number): GhRun | undefined {
  return state.exactRuns.get(branch)?.find((run) => run.id === runId);
}

function fairCandidates<T>(
  states: PollRepo[],
  configuredRepos: string[],
  cursor: number,
  values: (state: PollRepo) => T[],
): Array<{ state: PollRepo; value: T }> {
  const byRepo = new Map(states.map((state) => [state.repo, { state, values: values(state) }]));
  const orderedRepos = Array.from(
    { length: configuredRepos.length },
    (_, offset) => configuredRepos[(cursor + offset) % configuredRepos.length]!,
  );
  const output: Array<{ state: PollRepo; value: T }> = [];
  const max = Math.max(0, ...[...byRepo.values()].map((entry) => entry.values.length));
  for (let pass = 0; pass < max; pass += 1) {
    for (const repo of orderedRepos) {
      const entry = byRepo.get(repo);
      const value = entry?.values[pass];
      if (entry && value !== undefined) output.push({ state: entry.state, value });
    }
  }
  return output;
}

function successorIndex(configuredRepos: string[], repo: string): number {
  const index = configuredRepos.indexOf(repo);
  return index < 0 ? 0 : (index + 1) % configuredRepos.length;
}

function utcDate(date: Date): string {
  return date.toISOString().slice(0, 10);
}

function mergedQuery(searchQuery: string): string {
  return `query CiMergedPrSnapshot { search(type:ISSUE, first:100, query:${JSON.stringify(searchQuery)}) { issueCount pageInfo { hasNextPage } nodes { ... on PullRequest { number body url headRefName headRefOid createdAt mergedAt } } } }`;
}
