/**
 * AEGIS SOAR M3 — PROD reason-code confirmation (Rule 18 "right reason" airtightener).
 *
 * The primary disposal run (fincrime-m3-prod-two-eyes-disposal.ts) proved every leg
 * at the HTTP-status level (admin self-finalize 409 PAIRED with mlco finalize 200
 * DISPOSITIONED) and the durable read-backs confirmed the effect (sole approver=mlco,
 * no admin approval row, exactly one DISPOSITIONED case). A client-side extractor bug
 * left the 409 `reason` strings blank. This probe OBSERVES those reason codes directly:
 *
 *   • LOCK     — re-finalize the EXISTING disposed case (6d894ee0) as mlco. The case is
 *                already DISPOSITIONED, so the FOR UPDATE state check fires first ⇒ 409
 *                reason=state_conflict. This writes NOTHING (409 precedes any write).
 *   • SELF_APP — build ONE fresh case to PREPARED (admin maker), then admin self-finalize
 *                with the CORRECT reportHash so the ONLY firing guard is two-eyes ⇒ 409
 *                reason=self_approval. The case is then LEFT PARKED at PENDING_MLCO_REVIEW
 *                (a legitimate mid-workflow state; carry-listed, never finalized) so this
 *                does NOT create a second DISPOSITIONED row.
 *
 * Secrets (Rule 1): passwords from process.env, never printed; cookies/tokens never printed.
 * DEV/TEST tooling only.
 */

import { createHash } from "node:crypto";

const PROD_URL = process.env.PROD_URL ?? "https://aegis-cyber.replit.app";
const EXISTING_DISPOSED_CASE = "6d894ee0-2600-47f3-8d19-43d90f7ca7db";
const EXISTING_NARRATIVE_HASH = "5c13b2c56522fe75799ad1223051dc2998ba246132c638c8d65da47d5b2c4ddc";

function need(k: string): string {
  const v = process.env[k];
  if (!v) throw new Error(`missing required env ${k}`);
  return v;
}
const sha256 = (s: string): string => createHash("sha256").update(s).digest("hex");

class Jar {
  private c = new Map<string, string>();
  ingest(res: Response): void {
    const sc = (res.headers as unknown as { getSetCookie?: () => string[] }).getSetCookie?.() ?? [];
    for (const s of sc) {
      const p = s.split(";")[0];
      const i = p.indexOf("=");
      if (i > 0) this.c.set(p.slice(0, i).trim(), p.slice(i + 1).trim());
    }
  }
  header(): string { return [...this.c.entries()].map(([k, v]) => `${k}=${v}`).join("; "); }
}
interface R { status: number; json: Record<string, unknown> }
async function getCsrf(jar: Jar, xff: string): Promise<string> {
  const res = await fetch(`${PROD_URL}/api/csrf-token`, { headers: { cookie: jar.header(), "x-forwarded-for": xff } });
  jar.ingest(res);
  return ((await res.json().catch(() => ({}))) as { csrfToken?: string })?.csrfToken ?? "";
}
interface S { jar: Jar; csrf: string; xff: string }
async function login(username: string, password: string, xff: string): Promise<S> {
  const jar = new Jar();
  const res = await fetch(`${PROD_URL}/api/auth/login`, {
    method: "POST", headers: { "content-type": "application/json", "x-forwarded-for": xff },
    body: JSON.stringify({ username, password }),
  });
  jar.ingest(res);
  const j = (await res.json().catch(() => ({}))) as { success?: boolean };
  if (res.status !== 200 || j?.success !== true) throw new Error(`login(${username}) failed HTTP ${res.status}`);
  const csrf = await getCsrf(jar, xff);
  if (!csrf) throw new Error(`login(${username}) no CSRF`);
  return { jar, csrf, xff };
}
async function call(method: "GET" | "POST", path: string, s: S, body: unknown): Promise<R> {
  const headers: Record<string, string> = { "x-forwarded-for": s.xff, cookie: s.jar.header(), "x-csrf-token": s.csrf };
  if (body !== undefined) headers["content-type"] = "application/json";
  const res = await fetch(`${PROD_URL}${path}`, { method, headers, body: body !== undefined ? JSON.stringify(body) : undefined });
  return { status: res.status, json: (await res.json().catch(() => ({}))) as Record<string, unknown> };
}
const reason = (r: R): string =>
  String((r.json as { error?: { details?: { reason?: string } } })?.error?.details?.reason ?? "");

function allocateActorIps(n: number, runId = Date.now()): string[] {
  return Array.from({ length: n }, (_, i) => `203.0.113.${((Math.floor(runId / 1000) + i * 11) % 240) + 1}`);
}

async function main(): Promise<void> {
  console.log("=== AEGIS SOAR M3 — PROD reason-code confirmation ===");
  console.log(`${PROD_URL} | ${new Date().toISOString()}\n`);
  const ips = allocateActorIps(2);
  const admin = await login("admin", need("AEGIS_ADMIN_PASSWORD"), ips[0]);
  const mlco = await login("mlco", need("MLCO_PASSWORD"), ips[1]);
  console.log("logins OK: admin, mlco\n");

  const out: { leg: string; status: number; reason: string; pass: boolean; note: string }[] = [];

  // LOCK — re-finalize the existing DISPOSITIONED case as mlco (writes nothing)
  {
    const r = await call("POST", `/api/fincrime/cases/${EXISTING_DISPOSED_CASE}/finalize`, mlco,
      { reportHash: EXISTING_NARRATIVE_HASH, reason: "re-finalize a disposed case — must be locked" });
    const rs = reason(r);
    out.push({ leg: "LOCK (double-finalize on disposed case)", status: r.status, reason: rs,
      pass: r.status === 409 && rs === "state_conflict", note: "no new rows (409 precedes any write)" });
  }

  // SELF_APP — fresh case to PREPARED, then admin self-finalize (correct hash)
  const tag = `m3prodselfapprove-${Date.now()}`;
  const subjectRef = `opaque:${tag}-TEST_DELETE_OK`;
  const narrative = `PROD self-approval probe ${tag} — synthetic, delete ok`;
  const nHash = sha256(narrative);
  let parkedCaseId = "";
  {
    const ing = await call("POST", "/api/fincrime/ingest", admin,
      { sourceType: "MANUAL_REFERRAL", subjectRef, signal: { probe: tag } });
    parkedCaseId = String(ing.json?.caseId ?? "");
    if (ing.status !== 201 || !parkedCaseId) throw new Error(`ingest failed HTTP ${ing.status}`);
    await call("POST", `/api/fincrime/cases/${parkedCaseId}/start`, admin, {});
    await call("POST", `/api/fincrime/cases/${parkedCaseId}/escalate-mlco`, admin, { summary: `selfapprove probe ${tag}` });
    const prep = await call("POST", `/api/fincrime/cases/${parkedCaseId}/str`, admin, { narrative });
    if (prep.status !== 201 || prep.json?.narrativeHash !== nHash) throw new Error(`prepare failed HTTP ${prep.status}`);

    const r = await call("POST", `/api/fincrime/cases/${parkedCaseId}/finalize`, admin,
      { reportHash: nHash, reason: "maker attempts to approve own STR" });
    const rs = reason(r);
    out.push({ leg: "SELF_APP (maker self-finalize, correct hash)", status: r.status, reason: rs,
      pass: r.status === 409 && rs === "self_approval",
      note: `case ${parkedCaseId.slice(0, 8)}… LEFT PARKED at PENDING_MLCO_REVIEW (carry-list)` });
  }

  console.log("");
  for (const o of out) {
    console.log(`[${o.pass ? "PASS" : "FAIL"}] ${o.leg}`);
    console.log(`       HTTP ${o.status}  reason=${o.reason}`);
    console.log(`       ${o.note}`);
  }
  const ok = out.every((o) => o.pass);
  console.log(`\nRESULT: ${out.filter((o) => o.pass).length}/${out.length} reason codes OBSERVED as expected`);
  console.log(JSON.stringify({ parkedCaseId, parkedSubjectRef: subjectRef }, null, 2));
  if (!ok) process.exitCode = 1;
}
main().catch((e) => { console.error("FATAL:", e instanceof Error ? e.message : String(e)); process.exit(1); });
