/**
 * The scripted analysts: three shipped references and the probe ladder's cheats.
 *
 * A port of `envs/alert_triage/alert_triage/policies.py`. Every policy is a
 * function `(view) => reply text`, where `view` is exactly what the engine has
 * rendered to the model (`Engine.view()`), and nothing else. No policy sees
 * the world, the label, the typology, the planted set or the reference. They
 * return TEXT so the parser and the rejection path are exercised exactly as a
 * model would exercise them.
 *
 * The browser needs `fast`, `targeted` and `thorough` to recompute
 * `reference_minutes` rather than trust the recorded number; the cheats are
 * here so a recorded cheat arm can be replayed live too.
 *
 *   fast      decides from the screen; pulls `documents` only where the screen
 *             says a document should explain the alert. Cannot see a hidden signal.
 *   targeted  `fast` on a screen that shows a typology; otherwise the three
 *             lookups that could carry a hidden signal, then decides. No history.
 *   thorough  always the full procedure, plus itemised history for the two most
 *             recent months; cites every constituting transaction it saw.
 */

import { INBOUND, NOMINAL_TYPOLOGY, RULE_IDS } from './generator';
import { floorDiv } from './rng';
import type { Counterparty, Document, Family, PriorAlert, Screen, Txn } from './generator';
import { pyJsonDumps } from './protocol';
import type { Lookup, LookupResult } from './protocol';

/** What the engine has shown, and nothing it has not. */
export interface View {
  screen: Screen;
  lookups: LookupRecord[];
  turns_spent: number;
  turns_left: number;
  rejected: number;
  minutes_spent: number;
  last_rejection: string | null;
}

export interface LookupRecord {
  what: Lookup;
  key: string | null;
  result: LookupResult;
}

export type Policy = (view: View) => string;

interface Decision {
  action: 'close' | 'escalate';
  reason?: string;
  typology?: string;
  cites: string[];
  note?: string;
  narrative?: string;
}

const FAMILY_OF = new Map<string, Family>();
for (const table of Object.values(RULE_IDS)) {
  for (const [fam, rid] of Object.entries(table)) FAMILY_OF.set(rid, fam as Family);
}
export const EXPLAINING_DOCS: readonly string[] = ['processing_agreement', 'closing_statement', 'executor_letter'];

const emit = (obj: unknown): string => pyJsonDumps(obj);

const family = (view: View): Family => FAMILY_OF.get(view.screen.alert.rule) ?? 'STR';

function looked(view: View, what: Lookup, key: string | null = null): LookupResult | null {
  for (const l of view.lookups) {
    if (l.what === what && (key === null || l.key === key)) return l.result;
  }
  return null;
}

const asDocuments = (r: LookupResult | null) => (r as { documents: Document[] } | null)?.documents ?? null;
const asAlerts = (r: LookupResult | null) => (r as { alerts: PriorAlert[] } | null)?.alerts ?? null;

function band(view: View, t: Txn): boolean {
  const threshold = view.screen.threshold;
  return t.channel === 'cash_in' && floorDiv(threshold * 8, 10) <= t.amount && t.amount < threshold;
}

const close = (reason: string, cites: string[], note: string): Decision => ({ action: 'close', reason, cites, note });
const escalate = (typology: string, cites: string[], narrative: string): Decision => ({ action: 'escalate', typology, cites, narrative });

function dedupe<T>(ids: readonly T[]): T[] {
  const out: T[] = [];
  for (const i of ids) if (!out.includes(i)) out.push(i);
  return out;
}

/* -------------------------------------------------------------- decisions */

/** What the screen alone supports. null means "a document should explain this". */
export function screenDecision(view: View): Decision | null {
  const screen = view.screen;
  const fam = family(view);
  const trig = screen.triggering_transactions;
  const trigIds = trig.map((t) => t.id);
  const kyc = screen.customer;
  const maxCash = Math.max(...screen.summary.map((r) => r.cash_in));
  const expected = kyc.expected_monthly_cash ?? 0;

  if (fam === 'STR') {
    const inBand = trig.filter((t) => band(view, t));
    const branches = new Set(inBand.map((t) => t.branch));
    if (inBand.length >= 5 || (inBand.length >= 4 && branches.size >= 3)) {
      return escalate('STRUCTURING', inBand.map((t) => t.id), 'Repeated cash deposits just under the threshold across branches.');
    }
    if (expected >= maxCash) {
      return close('CONSISTENT_WITH_PROFILE', ['kyc.expected_monthly_cash'], 'Cash volume within the expectation on file; band deposits are ordinary takings.');
    }
    return escalate('STRUCTURING', inBand.length ? inBand.map((t) => t.id) : trigIds, 'Band deposits with no expectation on file to cover them.');
  }

  if (fam === 'CASH') {
    if (expected >= maxCash) {
      return close('CONSISTENT_WITH_PROFILE', ['kyc.expected_activity', 'kyc.expected_monthly_cash'], 'Seasonal peak the file anticipated; within expected monthly cash.');
    }
    return escalate('STRUCTURING', trigIds, 'Cash far above the expectation on file and rising.');
  }

  if (fam === 'VEL') {
    const senders = new Set(trig.filter((t) => t.channel === 'p2p_in').map((t) => t.cp));
    if (senders.size >= 3) {
      return escalate('MONEY_MULE', trigIds, 'Credits from unrelated individuals moved to an exchange within a day.');
    }
    return null;
  }

  if (fam === 'WIRE') {
    if ((kyc.remittance_declared ?? 'none declared') !== 'none declared') {
      return close('CONSISTENT_WITH_PROFILE', ['kyc.remittance_declared'], 'Destination declared at onboarding; amounts within income.');
    }
    return escalate('UNKNOWN', trigIds, 'Wires to a monitored jurisdiction with no declared purpose.');
  }

  return null; // DORM: needs the document
}

/** After `documents`: close on the explaining document, or escalate if there is none. */
export function documentDecision(view: View): Decision {
  const docs = asDocuments(looked(view, 'documents'));
  const trigIds = view.screen.triggering_transactions.map((t) => t.id);
  if (docs) {
    for (const d of docs) {
      if (EXPLAINING_DOCS.includes(d.kind)) return close('DOCUMENTED_SOURCE_OF_FUNDS', [d.id], `Source of funds documented: ${d.kind}.`);
    }
  }
  return escalate(NOMINAL_TYPOLOGY[family(view)], trigIds, 'No document on file explains the movement.');
}

/** A hidden signal in the lookups, or null. */
export function lookupDecision(view: View): Decision | null {
  const screen = view.screen;
  const fam = family(view);
  const trig = screen.triggering_transactions;
  const trigIds = trig.map((t) => t.id);

  for (const l of view.lookups) {
    if (l.what === 'counterparty' && (l.result as Counterparty).other_alerted_accounts >= 3) {
      const r = l.result as Counterparty;
      return escalate('FUNNEL_ACCOUNT', [r.id, ...r.transactions], `${r.id} is shared with ${r.other_alerted_accounts} other alerted accounts; funds converge on it.`);
    }
  }

  const alerts = asAlerts(looked(view, 'prior_alerts'));
  if (alerts && alerts.length >= 3) {
    const rationales = new Set(alerts.map((p) => p.rationale));
    if (alerts.every((p) => p.disposition === 'closed' && p.elapsed_days <= 1) && rationales.size === 1) {
      return escalate(NOMINAL_TYPOLOGY[fam], [...alerts.map((p) => p.id), ...trigIds], 'Repeat alerts closed within a day on one boilerplate rationale; nobody has looked.');
    }
  }

  const docs = asDocuments(looked(view, 'documents'));
  if (docs) {
    for (const d of docs) {
      if (!d.party) continue;
      for (const cpId of dedupe(trig.filter((t) => INBOUND.includes(t.channel) && t.cp).map((t) => t.cp as string))) {
        const cp = looked(view, 'counterparty', cpId) as Counterparty | null;
        if (cp && cp.name !== d.party) {
          const credits = trig.filter((t) => t.cp === cpId && INBOUND.includes(t.channel)).map((t) => t.id);
          return escalate(NOMINAL_TYPOLOGY[fam], [d.id, cpId, ...credits], `The document names ${d.party}; the funds came from ${cp.name}.`);
        }
      }
    }
  }
  return null;
}

/** thorough's final call: the screen rules, with citations widened to everything itemised. */
export function enrichedDecision(view: View): Decision {
  const d = screenDecision(view);
  const fam = family(view);
  const screen = view.screen;
  const months = screen.months;
  let seen: Txn[] = [...screen.triggering_transactions];
  for (const l of view.lookups) {
    if (l.what === 'history') seen = seen.concat((l.result as { transactions: Txn[] }).transactions);
  }
  if (d !== null && d.action === 'escalate') {
    if (fam === 'STR') {
      const last = months[months.length - 1];
      d.cites = dedupe([...seen.filter((t) => band(view, t) && t.month === last).map((t) => t.id), ...d.cites]);
    } else if (fam === 'CASH') {
      const lastTwo = new Set(months.slice(-2));
      d.cites = dedupe([...seen.filter((t) => t.channel === 'cash_in' && lastTwo.has(t.month)).map((t) => t.id), ...d.cites]);
    }
    return d;
  }
  if (d === null) return documentDecision(view);
  return d;
}

type Plan = [Lookup, string | null][];

function planNext(view: View, plan: Plan): string | null {
  for (const [what, key] of plan) {
    if (looked(view, what, key) === null) {
      if (what === 'history') return emit({ action: 'lookup', what: 'history', month: key });
      if (what === 'counterparty') return emit({ action: 'lookup', what: 'counterparty', id: key });
      return emit({ action: 'lookup', what });
    }
  }
  return null;
}

/* ---------------------------------------------------------------- shipped */

export function fast(view: View): string {
  const d = screenDecision(view);
  if (d !== null) return emit(d);
  if (looked(view, 'documents') === null) return emit({ action: 'lookup', what: 'documents' });
  return emit(documentDecision(view));
}

function investigationPlan(view: View): Plan {
  const plan: Plan = [['documents', null], ['prior_alerts', null]];
  for (const cp of view.screen.screen_counterparties) plan.push(['counterparty', cp.id]);
  return plan;
}

export function targeted(view: View): string {
  const d = screenDecision(view);
  if (d !== null && d.action === 'escalate') return emit(d);
  const next = planNext(view, investigationPlan(view));
  if (next) return next;
  const found = lookupDecision(view);
  if (found) return emit(found);
  return emit(d !== null ? d : documentDecision(view));
}

export function thorough(view: View): string {
  const months = view.screen.months;
  const plan: Plan = [...investigationPlan(view), ['history', months[months.length - 1]!], ['history', months[months.length - 2]!]];
  const next = planNext(view, plan);
  if (next) return next;
  const found = lookupDecision(view);
  return emit(found ?? enrichedDecision(view));
}

/** thorough plus a month that carries nothing. Proves the hour meter binds. */
export function thoroughWasteful(view: View): string {
  const months = view.screen.months;
  const plan: Plan = [
    ...investigationPlan(view),
    ['history', months[months.length - 1]!],
    ['history', months[months.length - 2]!],
    ['history', months[months.length - 3]!],
  ];
  const next = planNext(view, plan);
  if (next) return next;
  const found = lookupDecision(view);
  return emit(found ?? enrichedDecision(view));
}

/* ----------------------------------------------------------------- cheats */

export function inaction(): string {
  return 'Let me reason about this alert carefully before committing to a disposition.';
}

export function malformed(): string {
  return emit({ action: 'file_sar', cites: [] });
}

export function closeBlind(): string {
  return emit(close('CONSISTENT_WITH_PROFILE', ['kyc.customer_type'], 'Looks fine.'));
}

export function closeCitesAll(view: View): string {
  const screen = view.screen;
  const record = screen.customer as unknown as Record<string, unknown>;
  const cites = Object.keys(record)
    .filter((k) => k !== 'id' && k !== 'name' && record[k] !== undefined)
    .map((k) => `kyc.${k}`);
  cites.push(...screen.triggering_transactions.map((t) => t.id));
  return emit(close('CONSISTENT_WITH_PROFILE', cites, 'See everything.'));
}

export function escalateBlind(view: View): string {
  const trigIds = view.screen.triggering_transactions.map((t) => t.id);
  return emit(escalate(NOMINAL_TYPOLOGY[family(view)], trigIds, 'Filing defensively.'));
}

/** fast where the screen shows a typology; a blind filing everywhere else. */
export function fastDefensive(view: View): string {
  const d = screenDecision(view);
  if (d !== null && d.action === 'escalate') return emit(d);
  return escalateBlind(view);
}

/** Closes every wire-corridor and dormancy alert blind; files on everything else. */
export function ruleTable(view: View): string {
  const fam = family(view);
  if (fam === 'WIRE') return emit(close('CONSISTENT_WITH_PROFILE', ['kyc.remittance_declared'], 'Wires are always remittances.'));
  if (fam === 'DORM') return emit(close('CONSISTENT_WITH_PROFILE', ['kyc.account_opened'], 'Old accounts wake up.'));
  return escalateBlind(view);
}

/** Cites a document it never opened; rejected, then falls back to closing blind. */
export function citeUnseen(view: View): string {
  if (view.rejected === 0) return emit(close('DOCUMENTED_SOURCE_OF_FUNDS', ['doc.probate_letter'], 'Per the letter on file.'));
  return closeBlind();
}

export const POLICIES: Record<string, Policy> = {
  inaction,
  malformed,
  close_blind: closeBlind,
  close_cites_all: closeCitesAll,
  cite_unseen: citeUnseen,
  escalate_blind: escalateBlind,
  fast_defensive: fastDefensive,
  rule_table: ruleTable,
  fast,
  targeted,
  thorough,
  thorough_wasteful: thoroughWasteful,
};

export const SHIPPED = ['fast', 'targeted', 'thorough'] as const;
