import { verdictFromEarlierTurn, waitingOnHuman } from './attention';
import { costPill, formatClock } from './cost';
import { NO_RECORD, SUBAGENT_ROW, judgedHeadline, snapshotHeadline } from './headline';
import { IDLE_LIVE, type LiveState } from './live';
import { tableInstrument, type TableInstrument } from './table';
import type {
  PaqadChief,
  PaqadCi,
  PaqadClaim,
  PaqadStartPoint,
  PaqadCommitGroup,
  PaqadCost,
  PaqadCostLine,
  PaqadCostTokens,
  PaqadDeliveryPlan,
  PaqadFindingsScan,
  PaqadGate,
  PaqadGateStrength,
  PaqadProofStep,
  PaqadRepair,
  PaqadStageProof,
  PaqadStageRun,
  PaqadRung,
  PaqadHeadline,
  PaqadHighTable,
  PaqadHudSnapshot,
  PaqadHudStage,
  PaqadLastBlock,
  PaqadPlanProgress,
  PaqadPrState,
  PaqadScope,
  PaqadSeat,
  PaqadShipCommit,
  PaqadSignoff,
  PaqadSignoffState,
  PaqadSeatState,
  PaqadTone,
  PaqadTimelineEntry,
  PaqadTrackRecord,
  PaqadWorkflow,
} from './types';

export const NOT_YET = 'not yet';

export const HUD_DIR = '.paqad/session/hud';

export function safeSessionId(sessionId: string): string {
  return sessionId.replace(/[^A-Za-z0-9._-]/g, '_').replace(/^\.+/, '_') || 'unknown';
}

export function joinPath(root: string, rel: string): string {
  return /[\\/]$/.test(root) ? `${root}${rel}` : `${root}/${rel}`;
}

export function snapshotPath(root: string, sessionId: string): string {
  return joinPath(root, `${HUD_DIR}/${safeSessionId(sessionId)}.json`);
}

export function receiptPath(root: string, sessionId: string): string {
  return joinPath(root, `${HUD_DIR}/${safeSessionId(sessionId)}.receipt.md`);
}

export function lastBlockPath(root: string, sessionId: string): string {
  return joinPath(root, `${HUD_DIR}/${safeSessionId(sessionId)}.last-block.json`);
}

export const PENDING_DECISIONS_DIR = '.paqad/decisions/pending';

export function entrySentinelPath(root: string, sessionId: string): string {
  const key = sessionId.trim().replace(/[^A-Za-z0-9_-]/g, '_');
  return joinPath(
    root,
    key === '' ? '.paqad/.agent-entry-loaded' : `.paqad/.agent-entry-loaded.d/${key}`,
  );
}

export function heartbeatPath(root: string, sessionId: string): string {
  return joinPath(root, `${HUD_DIR}/${safeSessionId(sessionId)}.live`);
}

export const ORG_ENFORCED = "This rule is enforced by your organization's settings.";
export const ALLOW_ONCE_STALE_CLI =
  'Allow once could not take effect because paqad-ai is older than the HUD, so the edit stays blocked. Update paqad-ai and try again.';

export const RULE_STOP_FIX = 'Let Claude fix it';
export const RULE_STOP_ALLOW = 'Allow once, recorded';
export const RULE_STOP_SHOW = 'Show the rule';
export const RULE_STOP_NO_REASON = 'No reason';
export const RULE_STOP_FALSE_POSITIVE = 'It is a false positive';
export const RULE_STOP_HEADER = 'Rule stop';
export const RULE_STOP_REASON_HEADER = 'Reason';

export const OWN_QUESTION_HEADERS: readonly string[] = [RULE_STOP_HEADER, RULE_STOP_REASON_HEADER];

export interface RuleStop {
  rule: string;
  title: string | null;
  file: string;
  toolUseId: string;
  managed: boolean;
  sole: boolean;
  token: string;
}

type Rec = Record<string, unknown>;

function rec(value: unknown): Rec | null {
  return typeof value === 'object' && value !== null && !Array.isArray(value)
    ? (value as Rec)
    : null;
}

function str(value: unknown): string | null {
  return typeof value === 'string' && value.length > 0 ? value : null;
}

function num(value: unknown): number | null {
  return typeof value === 'number' && Number.isFinite(value) ? value : null;
}

function list(value: unknown): unknown[] {
  return Array.isArray(value) ? value : [];
}

const TONES: readonly PaqadTone[] = ['neutral', 'working', 'needs-you', 'good', 'warn', 'bad'];

function readScan(value: unknown): PaqadFindingsScan {
  const scan = rec(value);
  const status = scan?.status;
  return {
    status: status === 'open' || status === 'clean' ? status : 'did not run',
    total: num(scan?.total),
    blocking: scan?.blocking === true,
  };
}

function readLastBlock(raw: unknown, block: Rec | null): PaqadLastBlock {
  const rule = str(block?.rule);
  const file = str(block?.file);
  const text =
    str(raw) ??
    str(block?.text) ??
    str(block?.reason) ??
    (rule && file ? `${rule} stopped an edit to ${file}.` : 'A rule stopped an edit.');
  const title = str(block?.title);
  const toolUseId = str(block?.tool_use_id);
  const at = str(block?.at);
  const token = str(block?.token);
  return {
    text,
    ...(rule ? { rule } : {}),
    ...(title ? { title } : {}),
    ...(file ? { file } : {}),
    ...(toolUseId ? { tool_use_id: toolUseId } : {}),
    ...(typeof block?.managed === 'boolean' ? { managed: block.managed } : {}),
    ...(at ? { at } : {}),
    ...(token ? { token } : {}),
    ...(typeof block?.used === 'boolean' ? { used: block.used } : {}),
  };
}

export function freshRuleStop(
  text: string | null,
  sessionId: string,
  toolUseId: string | undefined,
): RuleStop | null {
  if (text === null || !toolUseId) return null;
  let raw: Rec | null;
  try {
    raw = rec(JSON.parse(text));
  } catch {
    return null;
  }
  const rule = str(raw?.rule);
  const file = str(raw?.file);
  const token = str(raw?.token);
  if (!raw || !rule || !file || !token) return null;
  if (raw.used === true || str(raw.session) !== sessionId) return null;
  if (str(raw.tool_use_id) !== toolUseId) return null;
  return {
    rule,
    title: str(raw.title),
    file,
    toolUseId,
    managed: raw.managed === true,
    sole: raw.sole === true,
    token,
  };
}

export function isDenied(result: unknown): boolean {
  const raw = rec(result);
  return raw !== null && (raw.deny !== undefined || raw.isError === true);
}

export type EditOutcome = 'landed' | 'still-blocked';

export function editOutcome(result: unknown): EditOutcome {
  return isDenied(result) ? 'still-blocked' : 'landed';
}

export function isAllowedOnce(text: string | null, stop: RuleStop): boolean {
  if (text === null) return false;
  let raw: Rec | null;
  try {
    raw = rec(JSON.parse(text));
  } catch {
    return false;
  }
  return raw?.allowed_once === true && str(raw.tool_use_id) === stop.toolUseId;
}

export function ruleStopQuestion(stop: RuleStop): string {
  return `${stop.rule} stopped this edit to ${stop.file}. What should happen?`;
}

export function ruleTextQuestion(stop: RuleStop): string {
  return `${stop.rule}: ${stop.title ?? 'no rule text recorded'}. What should happen?`;
}

export function overrideArgs(
  stop: RuleStop,
  sessionId: string,
  root: string,
  reason: string | null,
): string[] {
  return [
    '--rule',
    stop.rule,
    '--file',
    stop.file,
    '--token',
    stop.token,
    '--tool-use-id',
    stop.toolUseId,
    '--session',
    sessionId,
    '--project-root',
    root,
    ...(reason ? [`--reason=${reason}`] : []),
  ];
}

export function outcomeArgs(
  stop: RuleStop,
  sessionId: string,
  root: string,
  outcome: EditOutcome,
): string[] {
  return [
    '--outcome',
    outcome,
    '--rule',
    stop.rule,
    '--file',
    stop.file,
    '--tool-use-id',
    stop.toolUseId,
    '--session',
    sessionId,
    '--project-root',
    root,
  ];
}

function readTokens(value: unknown): PaqadCostTokens {
  const tokens = rec(value);
  const count = (key: string): number => Math.max(0, num(tokens?.[key]) ?? 0);
  return {
    input: count('input'),
    output: count('output'),
    cache_read: count('cache_read'),
    cache_write: count('cache_write'),
    total: count('total'),
  };
}

function readCostLine(value: unknown, dated: boolean): PaqadCostLine {
  const line = rec(value);
  return { tokens: readTokens(line?.tokens), usd: dated ? num(line?.usd) : null };
}

function readCost(value: unknown): PaqadCost | null {
  const cost = rec(value);
  if (!cost) return null;
  const date = str(cost.price_table_date);
  const dated = date !== null;
  const chief = rec(cost.chief);
  return {
    scope: cost.scope === 'change' ? 'change' : 'whole-session',
    price_table_date: date ?? '',
    label: str(cost.label) ?? 'estimate at API prices',
    basis: str(cost.basis) ?? 'API-equivalent estimate',
    plan_note: str(cost.plan_note) ?? '',
    sealed: rec(cost.window)?.sealed === true,
    tokens: readTokens(cost.tokens),
    usd: dated ? num(cost.usd) : null,
    unpriced_models: list(cost.unpriced_models).filter((m): m is string => typeof m === 'string'),
    by_stage: list(cost.by_stage).flatMap((entry) => {
      const stage = str(rec(entry)?.stage);
      return stage ? [{ stage, ...readCostLine(entry, dated) }] : [];
    }),
    by_expert: list(cost.by_expert).flatMap((entry) => {
      const role = str(rec(entry)?.role);
      return role
        ? [{ role, ...readCostLine(entry, dated), exact: rec(entry)?.exact === true }]
        : [];
    }),
    chief: chief ? { ...readCostLine(chief, dated), exact: chief.exact === true } : null,
    not_counted: list(cost.not_counted).filter((t): t is string => typeof t === 'string'),
    live: cost.live === true,
  };
}

const SEAT_STATES: readonly PaqadSeatState[] = ['seated', 'writing', 'done'];

function strings(value: unknown): string[] {
  return list(value).filter((entry): entry is string => typeof entry === 'string');
}

function readTrackRecord(value: unknown): PaqadTrackRecord | null {
  const record = rec(value);
  const changes = num(record?.changes);
  if (!record || changes === null || changes <= 0) return null;
  const count = (key: string) => Math.max(0, num(record[key]) ?? 0);
  return {
    changes,
    changed_spec: count('changed_spec'),
    empty: count('empty'),
    avg_tokens: count('avg_tokens'),
  };
}

function readSeat(value: unknown): PaqadSeat | null {
  const seat = rec(value);
  const role = str(seat?.role);
  if (!seat || !role) return null;
  return {
    role,
    reason: str(seat.reason),
    origin: str(seat.origin),
    state: SEAT_STATES.find((state) => state === seat.state) ?? 'seated',
    seated_at: str(seat.seated_at),
    started_at: str(seat.started_at),
    done_at: str(seat.done_at),
    budget_tokens: num(seat.budget_tokens),
    clamped: typeof seat.clamped === 'boolean' ? seat.clamped : null,
    tokens_estimate: num(seat.tokens_estimate),
    tokens: rec(seat.tokens) ? readTokens(seat.tokens) : null,
    tokens_exact: seat.tokens_exact === true && rec(seat.tokens) !== null,
    findings: num(seat.findings),
    late: seat.late === true,
    track_record: readTrackRecord(seat.track_record),
  };
}

function readChief(value: unknown): PaqadChief | null {
  const seat = readSeat(value);
  return seat ? { ...seat, verdict: str(rec(value)?.verdict) } : null;
}

function each<T>(value: unknown, pick: (entry: Rec) => T): T[] {
  return list(value).flatMap((entry) => {
    const record = rec(entry);
    return record ? [pick(record)] : [];
  });
}

function readTable(value: unknown): PaqadHighTable | null {
  const table = rec(value);
  if (!table) return null;
  const rulings = rec(table.rulings);
  const questions = rec(table.questions);
  return {
    skipped: table.skipped === true,
    met_after_freeze: table.met_after_freeze === true,
    seats: list(table.seats).flatMap((seat) => readSeat(seat) ?? []),
    chief: readChief(table.chief),
    rulings: rulings
      ? {
          accepted: strings(rulings.accepted),
          declined: each(rulings.declined, (e) => ({
            id: str(e.id) ?? '',
            reason: str(e.reason) ?? '',
          })),
          conflicts: each(rulings.conflicts, (e) => ({
            target: str(e.target) ?? '',
            recommendation: str(e.recommendation) ?? '',
            rationale: str(e.rationale) ?? '',
          })),
          merged: each(rulings.merged, (e) => ({
            target: str(e.target) ?? '',
            merged_claim: str(e.merged_claim) ?? '',
            rationale: str(e.rationale) ?? '',
          })),
          renames: each(rulings.renames, (e) => ({
            id: str(e.id) ?? '',
            target: str(e.target) ?? '',
          })),
          settled: each(rulings.settled, (e) => ({
            target: str(e.target) ?? '',
            chosen: str(e.chosen) ?? '',
            source: str(e.source) ?? '',
          })),
        }
      : null,
    gaps: each(table.gaps, (e) => ({
      area: str(e.area) ?? '',
      why_it_matters: str(e.why_it_matters) ?? '',
      question: str(e.question),
    })),
    questions: questions
      ? {
          asked: num(questions.asked) ?? 0,
          auto_answered: each(questions.auto_answered, (e) => ({
            question: str(e.question) ?? '',
            source: str(e.source) ?? '',
          })),
        }
      : null,
    credits: each(table.credits, (e) => ({ line: str(e.line) ?? '', roles: strings(e.roles) })),
  };
}

function readStep(value: unknown): string | null {
  if (value === null || value === undefined) return null;
  const step = rec(value);
  return str(value) ?? str(step?.state) ?? str(step?.status) ?? 'recorded';
}

function readPr(value: unknown): string | null {
  const pr = rec(value);
  const number = num(pr?.number);
  if (number === null) return readStep(value);
  return `${pr?.draft === true ? 'draft ' : ''}#${number}`;
}

function readCi(value: unknown): string | null {
  const ci = rec(value);
  const total = num(ci?.total);
  if (ci?.state === 'pending' && total !== null) return `${num(ci.done) ?? 0} of ${total} done`;
  return readStep(value);
}

function readCiDetail(value: unknown): PaqadCi | null {
  const ci = rec(value);
  const state = str(ci?.state);
  if (!ci || !state) return null;
  return {
    state,
    done: num(ci.done),
    total: num(ci.total),
    reason: str(ci.reason),
    jobs: each(ci.jobs, (job) => ({
      name: str(job.name) ?? 'check',
      status: str(job.status),
      conclusion: str(job.conclusion),
      started_at: str(job.started_at),
      completed_at: str(job.completed_at),
    })),
  };
}

const SIGNOFF_STATES: readonly PaqadSignoffState[] = [
  'pending',
  'writing',
  'awaiting_sign_off',
  'approved',
  'changes_requested',
  'skipped',
  'not_posted',
];

export function readSignoff(value: unknown): PaqadSignoff | null {
  const signoff = rec(value);
  const state = signoff?.state;
  if (!signoff || !SIGNOFF_STATES.includes(state as PaqadSignoffState)) return null;
  return {
    state: state as PaqadSignoffState,
    ref: str(signoff.ref),
    posted_at: str(signoff.posted_at),
    checked_at: str(signoff.checked_at),
    link: str(signoff.link),
  };
}

export function readDiff(
  value: unknown,
): { additions: number; deletions: number; partial: boolean } | null {
  const diff = rec(value);
  const additions = num(diff?.additions);
  const deletions = num(diff?.deletions);
  if (additions === null || deletions === null) return null;
  return { additions, deletions, partial: diff?.partial === true };
}

function readShipCommits(value: unknown): PaqadShipCommit[] {
  return each(value, (commit) => ({
    sha: str(commit.sha) ?? '',
    subject: str(commit.subject) ?? '',
    own: typeof commit.own === 'boolean' ? commit.own : null,
  })).filter((commit) => commit.sha !== '');
}

const PR_STATES: readonly PaqadPrState[] = ['open', 'merged', 'closed'];

function readPrState(value: unknown): PaqadPrState | null {
  return PR_STATES.find((state) => state === value) ?? null;
}

function readScope(value: unknown): PaqadScope {
  const scope = rec(value);
  return {
    qualified: scope?.qualified === true,
    reason: str(scope?.reason) ?? 'no scope recorded',
  };
}

function readWorkflow(value: unknown): PaqadWorkflow | null {
  const workflow = rec(value);
  const name = str(workflow?.name);
  if (!workflow || !name) return null;
  return {
    name,
    started_at: str(workflow.started_at),
    tokens: num(workflow.tokens),
    usd: num(workflow.usd),
    unpriced: strings(workflow.unpriced),
  };
}

const GATE_STRENGTHS: readonly PaqadGateStrength[] = ['deterministic', 'llm-judged'];

function readGate(value: unknown): PaqadGate | null {
  const gate = rec(value);
  const name = str(gate?.name);
  if (!gate || !name) return null;
  const verdict = str(gate.verdict) ?? 'pending';
  return {
    name,
    verdict,
    required: typeof gate.required === 'boolean' ? gate.required : verdict !== 'not-required',
    strength: GATE_STRENGTHS.find((strength) => strength === gate.strength) ?? null,
    ...(gate.owner === 'work' || gate.owner === 'paqad-record' ? { owner: gate.owner } : {}),
  };
}

const VERDICT_KINDS = ['pass', 'fail', 'inconclusive'] as const;

const RUNGS: readonly PaqadRung[] = ['done', 'committed', 'pr-open'];

function readRung(value: unknown): { rung?: PaqadRung } {
  const rung = RUNGS.find((known) => known === value);
  return rung ? { rung } : {};
}

function readHeadline(headline: Rec | null, tone: PaqadTone): PaqadHeadline {
  const row = num(headline?.row);
  const word = str(headline?.word);
  const note = str(headline?.note);
  const gap = str(headline?.gap);
  const clockFrom = str(headline?.clock_from);
  return {
    text: str(headline?.text) ?? '',
    tone,
    ...(row !== null ? { row } : {}),
    ...(word ? { word } : {}),
    ...readRung(headline?.rung),
    ...(note ? { note } : {}),
    ...(gap ? { gap } : {}),
    ...(clockFrom ? { clock_from: clockFrom } : {}),
  };
}

function readVerdictParts(verdict: Record<string, unknown>): {
  work_kind?: (typeof VERDICT_KINDS)[number];
  work_reasons?: string[];
  record_gaps?: string[];
} {
  const workKind = VERDICT_KINDS.find((kind) => kind === verdict.work_kind);
  return {
    ...(workKind ? { work_kind: workKind } : {}),
    ...(Array.isArray(verdict.work_reasons) ? { work_reasons: strings(verdict.work_reasons) } : {}),
    ...(Array.isArray(verdict.record_gaps) ? { record_gaps: strings(verdict.record_gaps) } : {}),
  };
}

const STEP_STATES: ReadonlyArray<PaqadProofStep['state']> = ['done', 'failed', 'not-yet'];

function readProof(value: unknown): PaqadStageProof[] {
  return each(value, (entry) => ({
    stage: str(entry.stage) ?? '',
    words: str(entry.words) ?? str(entry.stage) ?? '',
    state: str(entry.state) ?? 'missing',
    steps: each(entry.steps, (row) => ({
      id: str(row.id) ?? '',
      words: str(row.words) ?? '',
      detail: str(row.detail),
      state: STEP_STATES.find((state) => state === row.state) ?? 'not-yet',
    })).filter((row) => row.words !== ''),
    gates: each(entry.gates, (row) => ({
      code: str(row.code) ?? '',
      words: str(row.words) ?? '',
      state: str(row.state) ?? 'not-judged',
    })).filter((row) => row.words !== ''),
    ...readRun(entry.run),
  })).filter((entry) => entry.stage !== '');
}

function readRun(value: unknown): { run?: PaqadStageRun } {
  const run = rec(value);
  if (!run) return {};
  const line = str(run.line);
  const short = str(run.short_line);
  const report = str(run.report);
  const recipe = str(run.recipe);
  if (!line || !short || !report || !recipe) return {};
  const tone = run.tone === 'fail' || run.tone === 'cau' ? run.tone : 'dim';
  return {
    run: {
      line,
      short_line: short,
      report,
      recipe,
      tone,
      runs: typeof run.runs === 'number' ? run.runs : 1,
      needs_look: run.needs_look === true,
      footnote: str(run.footnote),
    },
  };
}

function readRepairs(value: unknown): PaqadRepair[] {
  return list(value).flatMap((entry) => {
    const what = str(rec(entry)?.what);
    return what ? [{ what, at: str(rec(entry)?.at) }] : [];
  });
}

const START_SOURCES = ['start_commit', 'fork-point', 'fallback-base'] as const;

function readStartPoint(value: unknown): { start_point?: PaqadStartPoint } {
  const start = rec(value);
  const source = START_SOURCES.find((known) => known === start?.source);
  if (!start || !source) return {};
  return {
    start_point: { source, base: str(start.base), files_known: start.files_known !== false },
  };
}

function readClaim(value: unknown): PaqadClaim | null {
  const claim = rec(value);
  const kind = str(claim?.kind);
  if (!claim || !kind) return null;
  return {
    kind,
    id: str(claim.id),
    label: str(claim.label) ?? kind,
    verdict: str(claim.verdict) ?? 'inconclusive',
    test_id: str(claim.test_id),
  };
}

const TIMELINE_TEXT = [
  'agent',
  'verdict',
  'title',
  'issue',
  'lane',
  'sha',
  'subject',
  'state',
  'decision',
] as const;

const TIMELINE_COUNTS = [
  'duration_ms',
  'count',
  'picked',
  'standing',
  'findings',
  'number',
  'requirements',
  'criteria',
] as const;

function readTimelineEntry(entry: Record<string, unknown>): PaqadTimelineEntry {
  const read: PaqadTimelineEntry = {
    at: str(entry.at) ?? '',
    kind: str(entry.kind) ?? 'unknown',
    stage: str(entry.stage),
    note: str(entry.note),
  };
  for (const key of TIMELINE_TEXT) {
    const value = str(entry[key]);
    if (value !== null) read[key] = value;
  }
  for (const key of TIMELINE_COUNTS) {
    const value = num(entry[key]);
    if (value !== null) read[key] = value;
  }
  if (typeof entry.own_agent === 'boolean') read.own_agent = entry.own_agent;
  return read;
}

function readDeliveryPlan(value: unknown): PaqadDeliveryPlan | null {
  const plan = rec(value);
  if (!plan) return null;
  const { commit, push, pr, ci, merge } = plan;
  if (![commit, push, pr, ci, merge].every((step) => typeof step === 'boolean')) return null;
  return {
    commit: commit === true,
    push: push === true,
    pr: pr === true,
    ci: ci === true,
    merge: merge === true,
    reason: str(plan.reason),
  };
}

export const STAGE_TIMELINE_KINDS: ReadonlySet<string> = new Set([
  'open',
  'close',
  'stage_start',
  'stage_end',
]);

export function fixProofClaims(snapshot: PaqadHudSnapshot): PaqadClaim[] {
  return snapshot.claims.filter((claim) => claim.kind === 'fix-proof');
}

function readPlan(value: unknown): PaqadPlanProgress | null {
  const plan = rec(value);
  const steps = num(plan?.steps);
  const done = num(plan?.done);
  return steps !== null && steps > 0 && done !== null ? { steps, done } : null;
}

function readBundleFiles(value: unknown): string[] {
  return each(value, (entry) => (entry.present === true ? str(entry.file) : null)).filter(
    (file): file is string => file !== null,
  );
}

function readCommitGroup(value: unknown): PaqadCommitGroup | null {
  const group = rec(value);
  if (!group) return null;
  return {
    count: num(group.count) ?? 0,
    tickets: strings(group.tickets),
    merges: num(group.merges) ?? 0,
    untagged: num(group.untagged) ?? 0,
  };
}

function readClosedChange(value: unknown): Pick<PaqadHudSnapshot, 'closed_change'> {
  const closed = rec(value);
  const at = str(closed?.closed_at);
  if (!closed || !at) return {};
  return {
    closed_change: {
      closed_at: at,
      word: str(closed.word),
      new_commits: num(closed.new_commits) ?? 0,
    },
  };
}

export function parseSnapshot(text: string | null | undefined): PaqadHudSnapshot | null {
  if (typeof text !== 'string' || text.trim() === '') return null;
  let raw: unknown;
  try {
    raw = JSON.parse(text);
  } catch {
    return null;
  }
  const snap = rec(raw);
  if (!snap || typeof snap.schema !== 'string' || !snap.schema.startsWith('paqad.hud/')) {
    return null;
  }
  const headline = rec(snap.headline);
  const tone = TONES.find((t) => t === headline?.tone) ?? 'neutral';
  const change = rec(snap.change);
  const rules = rec(snap.rules);
  const findings = rec(snap.findings);
  const decisions = rec(snap.decisions);
  const ship = rec(snap.ship);
  const verdict = rec(snap.verdict);
  const kind = verdict?.kind;
  const lastBlock = rec(snap.last_block);
  const writtenAt = str(verdict?.written_at);
  const receiptFile = str(rec(snap.receipt)?.path);
  const scope = readScope(snap.scope);
  const parsed: PaqadHudSnapshot = {
    session_id: str(snap.session_id) ?? '',
    generated_at: str(snap.generated_at) ?? '',
    scope,
    workflow: readWorkflow(snap.workflow),
    active: snap.active === true && scope.qualified,
    route: str(snap.route),
    lane: str(snap.lane),
    ...(Array.isArray(snap.lane_stages)
      ? { lane_stages: list(snap.lane_stages).filter((s): s is string => typeof s === 'string') }
      : {}),
    lane_reason: str(snap.lane_reason),
    headline: readHeadline(headline, tone),
    change: change
      ? {
          dir: str(change.dir),
          id: str(change.id),
          issue: str(change.issue),
          title: str(change.title),
          branch: str(change.branch),
        }
      : null,
    paused: list(snap.paused).map((entry) => ({
      workflow: str(rec(entry)?.workflow) ?? 'unknown',
      lane: str(rec(entry)?.lane),
    })),
    stages: list(snap.stages).flatMap((entry): PaqadHudStage[] => {
      const stage = rec(entry);
      const name = str(stage?.stage);
      if (!stage || !name) return [];
      return [
        {
          stage: name,
          state: str(stage.state) ?? 'missing',
          mandatory: stage.mandatory === true,
          started_at: str(stage.started_at),
          ended_at: str(stage.ended_at),
          ...(stage.first_started_at !== undefined
            ? { first_started_at: str(stage.first_started_at) }
            : {}),
          ...(stage.last_ended_at !== undefined ? { last_ended_at: str(stage.last_ended_at) } : {}),
          ...(num(stage.runs) !== null ? { runs: num(stage.runs)! } : {}),
          ...(stage.record_gap === true ? { record_gap: true } : {}),
          ...(stage.no_report === true ? { no_report: true } : {}),
          duration_unreliable: stage.duration_unreliable === true,
          duration_ms: num(stage.duration_ms),
          agent: str(stage.agent),
        },
      ];
    }),
    timeline: each(snap.timeline, readTimelineEntry).filter((entry) => entry.at !== ''),
    table: readTable(snap.table),
    gates: list(snap.gates).flatMap((entry) => readGate(entry) ?? []),
    gates_expected: strings(snap.gates_expected),
    claims: list(snap.claims).flatMap((entry) => readClaim(entry) ?? []),
    bundle_files: readBundleFiles(snap.evidence_expected),
    flags: list(snap.flags).flatMap((entry) => {
      const flag = rec(entry);
      const id = str(flag?.id);
      return id ? [{ id, text: str(flag?.text) ?? id }] : [];
    }),
    rules: {
      recorded: rules?.recorded === true,
      applicable: num(rules?.applicable),
      loaded_at: str(rules?.loaded_at),
    },
    findings: findings
      ? { rules: readScan(findings.rules), near_copies: readScan(findings.near_copies) }
      : null,
    decisions: {
      pending: list(decisions?.pending).map((entry) => ({
        id: str(rec(entry)?.id) ?? 'unknown',
        category: str(rec(entry)?.category) ?? 'unknown',
        question: str(rec(entry)?.question) ?? '',
      })),
      resolved: num(decisions?.resolved) ?? 0,
    },
    last_block:
      snap.last_block === null || snap.last_block === undefined
        ? null
        : readLastBlock(snap.last_block, lastBlock),
    delivery_plan: readDeliveryPlan(snap.delivery_plan),
    plan: readPlan(snap.plan),
    ship: ship
      ? {
          commits: num(ship.commits) ?? 0,
          commit_decision: str(ship.commit_decision),
          merge_commit: str(ship.merge_commit),
          push: ship.push_current === false ? null : readStep(ship.push),
          pr: readPr(ship.pr),
          ci: readCi(ship.ci),
          push_branch: str(rec(ship.push)?.branch),
          pr_url: str(rec(ship.pr)?.url),
          ci_detail: readCiDetail(ship.ci),
          own: readCommitGroup(ship.own),
          inherited: readCommitGroup(ship.inherited),
          commit_list: readShipCommits(ship.commit_list),
          inherited_count: num(ship.inherited_count) ?? 0,
          pr_number: num(rec(ship.pr)?.number),
          pr_state: readPrState(ship.pr_state),
          pr_head: str(rec(ship.pr)?.head_sha),
          pr_title: str(ship.pr_title),
          pr_files: num(ship.pr_files),
          pr_additions: num(ship.pr_additions),
          pr_deletions: num(ship.pr_deletions),
          diff: readDiff(ship.diff),
          files_changed: num(ship.files_changed),
          ci_read_at: str(ship.ci_read_at),
          all_green: ship.all_green === true,
          merged: ship.merged === true,
        }
      : null,
    verdict:
      verdict && (kind === 'pass' || kind === 'fail' || kind === 'inconclusive')
        ? {
            word: str(verdict.word) ?? VERDICT_WORDS[kind],
            kind,
            reasons: list(verdict.reasons).filter((r): r is string => typeof r === 'string'),
            ...(writtenAt ? { at: writtenAt } : {}),
            ...readVerdictParts(verdict),
          }
        : null,
    receipt: receiptFile ? { path: receiptFile } : null,
    cost: readCost(snap.cost),
    signoff: readSignoff(snap.signoff),
    ...(Array.isArray(snap.record_gaps) ? { record_gaps: strings(snap.record_gaps) } : {}),
    ...(Array.isArray(snap.repairs) ? { repairs: readRepairs(snap.repairs) } : {}),
    ...readRung(snap.rung),
    ...(snap.verdict_stale === true ? { verdict_stale: true as const } : {}),
    ...readStartPoint(snap.start_point),
    ...(Array.isArray(snap.proof) ? { proof: readProof(snap.proof) } : {}),
    ...(typeof snap.ledger_note === 'string' ? { ledger_note: snap.ledger_note } : {}),
    ...readClosedChange(snap.closed_change),
  };
  return scope.qualified ? parsed : idleSnapshot(parsed);
}

function idleSnapshot(snapshot: PaqadHudSnapshot): PaqadHudSnapshot {
  return {
    ...snapshot,
    active: false,
    change: null,
    stages: [],
    timeline: [],
    table: null,
    gates: [],
    claims: [],
    bundle_files: [],
    flags: [],
    findings: null,
    decisions: { pending: [], resolved: 0 },
    plan: null,
    ship: null,
    verdict: null,
    receipt: null,
    cost: null,
    signoff: null,
    record_gaps: undefined,
    repairs: undefined,
    rung: undefined,
    verdict_stale: undefined,
    start_point: undefined,
    proof: undefined,
  };
}

export const VERDICT_WORDS = {
  pass: 'Safe to merge',
  fail: 'Needs your attention',
  inconclusive: 'Inconclusive',
} as const;

export const BAND_STATES = [
  'Ready',
  'Working',
  'Branch warning',
  'Waiting for your answer',
  'Inconclusive',
  'Safe to merge',
  'Needs your attention',
  'Paused',
] as const;

export type BandState = string;

const STATE_TONE: Record<(typeof BAND_STATES)[number], PaqadTone> = {
  Ready: 'neutral',
  Working: 'working',
  'Branch warning': 'warn',
  'Waiting for your answer': 'needs-you',
  Inconclusive: 'warn',
  'Safe to merge': 'good',
  'Needs your attention': 'bad',
  Paused: 'neutral',
};

function isBranchFlag(flag: { id: string }): boolean {
  return flag.id.startsWith('branch');
}

export function bandState(
  snapshot: PaqadHudSnapshot | null,
  live: LiveState = IDLE_LIVE,
): BandState {
  if (!snapshot) return 'Ready';
  if (waitingOnHuman(snapshot, live)) return 'Waiting for your answer';
  if (!snapshot.active) {
    if (snapshot.paused.length > 0) return 'Paused';
    if (snapshot.route && snapshot.route !== 'no-workflow') return 'Working';
    return 'Ready';
  }
  if (judgedHeadline(snapshot)) return snapshot.headline.word ?? 'Working';
  if (snapshot.flags.some(isBranchFlag)) return 'Branch warning';
  if (snapshot.stages.some((stage) => stage.state === 'running')) return 'Working';
  if (snapshot.verdict && !verdictFromEarlierTurn(snapshot, live)) {
    return VERDICT_WORDS[snapshot.verdict.kind];
  }
  return 'Working';
}

export function bandTone(state: BandState, flags: number): PaqadTone {
  const tone = STATE_TONE[state as (typeof BAND_STATES)[number]] ?? 'neutral';
  if (flags > 0 && (tone === 'good' || tone === 'working' || tone === 'neutral')) return 'warn';
  return tone;
}

export const STAGE_ORDER = [
  ['ticket_intake', 'Intake'],
  ['planning', 'Planning'],
  ['specification', 'Spec'],
  ['development', 'Build'],
  ['review', 'Review'],
  ['checks', 'Checks'],
  ['documentation_sync', 'Docs'],
  ['delivery', 'Deliver'],
] as const;

export function stageLabel(stage: string): string {
  if (stage === 'orchestrator') return 'Between stages';
  return STAGE_ORDER.find(([id]) => id === stage)?.[1] ?? stage;
}

export type SegmentState = 'done' | 'running' | 'marked' | 'failed' | 'not required' | 'not yet';

export interface StageSegment {
  stage: string;
  label: string;
  state: SegmentState;
  started_at: string | null;
}

function checksBacked(snapshot: PaqadHudSnapshot): boolean {
  return snapshot.gates.some((gate) => gate.name === 'code-tests-lint' && gate.verdict === 'pass');
}

const MANDATORY_STAGE_IDS = [
  'planning',
  'specification',
  'development',
  'review',
  'checks',
  'documentation_sync',
];

function outsideLane(stage: string, snapshot: PaqadHudSnapshot): boolean {
  if (!MANDATORY_STAGE_IDS.includes(stage)) return false;
  if (snapshot.lane_stages) return !snapshot.lane_stages.includes(stage);
  return stage === 'specification' && snapshot.lane === 'fast';
}

function segmentState(stage: PaqadHudStage | undefined, snapshot: PaqadHudSnapshot): SegmentState {
  if (!stage) return NOT_YET;
  if (
    (stage.record_gap || stage.no_report) &&
    stage.state !== 'running' &&
    stage.state !== 'failed'
  )
    return 'marked';
  switch (stage.state) {
    case 'complete':
    case 'redone':
      if (stage.duration_unreliable) return 'marked';
      if (stage.stage === 'checks' && !checksBacked(snapshot)) return 'marked';
      return 'done';
    case 'running':
      return 'running';
    case 'failed':
      return 'failed';
    case 'inconclusive':
      return 'marked';
    case 'skipped':
    case 'not-applicable':
      return 'not required';
    default:
      if (outsideLane(stage.stage, snapshot)) return 'not required';
      return NOT_YET;
  }
}

export function stageSegments(snapshot: PaqadHudSnapshot | null): StageSegment[] {
  return STAGE_ORDER.map(([stage, label]) => {
    const record = snapshot?.stages.find((entry) => entry.stage === stage);
    return {
      stage,
      label,
      state: snapshot && snapshot.active ? segmentState(record, snapshot) : NOT_YET,
      started_at: record?.started_at ?? null,
    };
  });
}

export { formatClock };

export function spanText(ms: number): string {
  const minutes = Math.floor(Math.max(0, ms) / 60_000);
  const hours = Math.floor(minutes / 60);
  return hours > 0 ? `${hours} h ${minutes % 60} min` : `${minutes} min`;
}

export interface StageRuns {
  runs: string;
  span: string | null;
}

export function stageRuns(stage: PaqadHudStage | undefined, now: number | null): StageRuns | null {
  const runs = stage?.runs ?? 1;
  if (!stage || runs <= 1) return null;
  const first = Date.parse(stage.first_started_at ?? stage.started_at ?? '');
  const last =
    stage.state === 'running' ? (now ?? Number.NaN) : Date.parse(stage.last_ended_at ?? '');
  const span = Number.isFinite(first) && Number.isFinite(last) ? spanText(last - first) : null;
  return { runs: `${runs} runs`, span };
}

export function liveClock(segments: StageSegment[], now: number): string | null {
  const running = segments.find((segment) => segment.state === 'running');
  const started = running?.started_at ? Date.parse(running.started_at) : Number.NaN;
  return Number.isFinite(started) ? formatClock(now - started) : null;
}

export interface BandView {
  state: BandState;
  tone: PaqadTone;
  headline: string;
  note: string | null;
  gap: string | null;
  activity: string[];
  flags: number;
  stages: StageSegment[];
  clock: string | null;
  cost: string;
  table: TableInstrument | null;
}

export function headlineAfterState(view: Pick<BandView, 'state' | 'headline'>): string {
  return view.headline.startsWith(view.state)
    ? view.headline.slice(view.state.length).replace(/^[.:]\s*/, '')
    : view.headline;
}

export function flagPill(flags: number): string | null {
  return flags > 0 ? `${flags} flag${flags === 1 ? '' : 's'}` : null;
}

export { costPill };

export function buildBandView(
  snapshot: PaqadHudSnapshot | null,
  now: number,
  live: LiveState = IDLE_LIVE,
): BandView {
  const state = bandState(snapshot, live);
  const flags = snapshot?.flags.length ?? 0;
  const stages = stageSegments(snapshot);
  const head = snapshot ? snapshotHeadline(snapshot, now, live) : null;
  const judged = judgedHeadline(snapshot) || live.waitingOn !== null || head?.row === SUBAGENT_ROW;
  return {
    state,
    tone: judged && head ? head.tone : bandTone(state, flags),
    headline: head ? head.text : NO_RECORD,
    note: head?.note ?? null,
    gap: head?.gap ?? null,
    activity: head?.activity ?? [],
    flags,
    stages,
    clock: liveClock(stages, now),
    cost: costPill(snapshot?.cost ?? null),
    table: tableInstrument(snapshot),
  };
}

export function isLive(snapshot: PaqadHudSnapshot | null): boolean {
  return snapshot?.active === true && snapshot.stages.some((stage) => stage.state === 'running');
}

export function isStageAgent(agent: { type?: unknown; status?: unknown }): boolean {
  return (
    typeof agent.type === 'string' &&
    /^(paqad:)?paqad-/.test(agent.type) &&
    (agent.status === 'running' || agent.status === 'pending' || agent.status === 'waiting')
  );
}

export function isRefreshTool(tool: string, input: { command?: unknown }): boolean {
  if (tool === 'Edit' || tool === 'Write' || tool === 'NotebookEdit') return true;
  if (tool === 'Agent' || tool === 'Task') return true;
  return tool === 'Bash' && typeof input.command === 'string' && input.command.includes('paqad-ai');
}
