/**
 * Phase 5 — parent → child memory handoff.
 *
 * When the parent piece spawns a subtask, we serialize the parent's
 * `WorkspaceMemorySnapshot` to `<child-workspace>/input/memory-handoff.json`
 * so the child piece-runner can absorb it on startup. Child sees parent
 * facts/decisions tagged with provenance lineage; the child's
 * `renderMemorySnapshot` shows them with portability/lineage cues so the
 * LLM treats workspace_local entries as "needs re-verification" rather
 * than as established truth.
 *
 * Codex review reflection:
 *   - schema versioned (v1) + atomic JSON write
 *   - sensitive-keyword filter on the way out (defensive — full PII
 *     detection is out of scope)
 *   - handoff size cap mirrors delta size cap
 *   - portability is preserved as-is; we never re-promote workspace_local
 *     to portable across boundaries
 */

import { join } from 'node:path';
import type {
  Fact,
  Decision,
  OpenQuestion,
  Portability,
  EvidenceKind,
  LineageEntry,
  WorkspaceMemorySnapshot,
} from './workspace-memory.js';
import { writeAtomicJson, readSafeJson, type AtomicJsonSchema } from './atomic-json.js';
import { logger } from '../../logger.js';

export const MEMORY_HANDOFF_FILE = 'input/memory-handoff.json';
export const MEMORY_HANDOFF_VERSION = 1 as const;

export const HANDOFF_LIMITS = {
  facts: 50,
  decisions: 30,
  openQuestions: 30,
  doNotRepeat: 30,
  /** Total stringified JSON budget. */
  byteSize: 256 * 1024,
} as const;

/** Strings that, if present in a claim/text, cause the entry to be filtered
 * out of the handoff. Codex review: minimum-viable secret defense. Full
 * PII detection is out of scope for Phase 5. */
const SENSITIVE_PATTERNS: readonly RegExp[] = [
  /\bpassword\b/i,
  /\bapi[_-]?key\b/i,
  /\bsecret\b/i,
  /\btoken\b/i,
  /\bbearer\s+[a-z0-9._-]+/i,
];

/** A fact serialized for handoff transport. Mirrors the in-memory Fact
 * shape but excludes runtime-only fields (id, sourceMovement). The
 * receiver mints a fresh id and treats sourceMovement as the receiver's
 * own. */
export interface HandoffFact {
  claim: string;
  confidence: 'high' | 'medium' | 'low';
  evidencePaths: string[];
  evidenceUrls: string[];
  observedAt: string;
  portability: Portability;
  evidenceKind: EvidenceKind;
  lineage: LineageEntry[];
}

export interface HandoffDecision {
  text: string;
  evidencePaths: string[];
  evidenceUrls: string[];
  decidedAt: string;
  portability: Portability;
  evidenceKind: EvidenceKind;
  lineage: LineageEntry[];
}

export interface HandoffOpenQuestion {
  question: string;
  createdAt: string;
}

export interface MemoryHandoff {
  version: typeof MEMORY_HANDOFF_VERSION;
  handoffId: string;
  parentJobId: string;
  parentWorkspaceRelative: string;
  createdAt: string;
  facts: HandoffFact[];
  decisions: HandoffDecision[];
  openQuestions: HandoffOpenQuestion[];
  doNotRepeat: string[];
  truncated?: { facts: number; decisions: number; openQuestions: number; doNotRepeat: number };
  filteredSensitive?: { facts: number; decisions: number };
}

export interface BuildHandoffInput {
  snapshot: WorkspaceMemorySnapshot;
  parentJobId: string;
  /** Path from child's workspace to parent's; today this is fixed at "../.." but
   * we accept it as a parameter so a future deeper layout doesn't break us. */
  parentWorkspaceRelative: string;
  /** uuid; pass an explicit one in tests for determinism. */
  handoffId: string;
  now?: string;
}

function looksSensitive(text: string): boolean {
  return SENSITIVE_PATTERNS.some((re) => re.test(text));
}

function projectFact(f: Fact): HandoffFact {
  return {
    claim: f.claim,
    confidence: f.confidence,
    evidencePaths: f.evidencePaths,
    evidenceUrls: f.evidenceUrls,
    observedAt: f.observedAt,
    portability: f.portability,
    evidenceKind: f.evidenceKind,
    lineage: f.lineage,
  };
}

function projectDecision(d: Decision): HandoffDecision {
  return {
    text: d.text,
    evidencePaths: d.evidencePaths,
    evidenceUrls: d.evidenceUrls,
    decidedAt: d.decidedAt,
    portability: d.portability,
    evidenceKind: d.evidenceKind,
    lineage: d.lineage,
  };
}

function projectOpenQuestion(q: OpenQuestion): HandoffOpenQuestion {
  return { question: q.question, createdAt: q.createdAt };
}

/**
 * Build a MemoryHandoff payload from the parent's snapshot, applying the
 * sensitive-keyword filter and the size limits. Returns the payload plus
 * a counts breakdown for logging.
 */
export function buildMemoryHandoff(input: BuildHandoffInput): MemoryHandoff {
  const sensitiveFacts = input.snapshot.facts.filter((f) => looksSensitive(f.claim));
  const sensitiveDecisions = input.snapshot.decisions.filter((d) => looksSensitive(d.text));

  const safeFacts = input.snapshot.facts.filter((f) => !looksSensitive(f.claim));
  const safeDecisions = input.snapshot.decisions.filter((d) => !looksSensitive(d.text));

  const truncated = { facts: 0, decisions: 0, openQuestions: 0, doNotRepeat: 0 };

  const cappedFacts = safeFacts.slice(0, HANDOFF_LIMITS.facts);
  truncated.facts = safeFacts.length - cappedFacts.length;

  const cappedDecisions = safeDecisions.slice(0, HANDOFF_LIMITS.decisions);
  truncated.decisions = safeDecisions.length - cappedDecisions.length;

  const cappedOpenQuestions = input.snapshot.openQuestions.slice(0, HANDOFF_LIMITS.openQuestions);
  truncated.openQuestions = input.snapshot.openQuestions.length - cappedOpenQuestions.length;

  const cappedDoNotRepeat = input.snapshot.doNotRepeat.slice(0, HANDOFF_LIMITS.doNotRepeat);
  truncated.doNotRepeat = input.snapshot.doNotRepeat.length - cappedDoNotRepeat.length;

  const handoff: MemoryHandoff = {
    version: MEMORY_HANDOFF_VERSION,
    handoffId: input.handoffId,
    parentJobId: input.parentJobId,
    parentWorkspaceRelative: input.parentWorkspaceRelative,
    createdAt: input.now ?? new Date().toISOString(),
    facts: cappedFacts.map(projectFact),
    decisions: cappedDecisions.map(projectDecision),
    openQuestions: cappedOpenQuestions.map(projectOpenQuestion),
    doNotRepeat: cappedDoNotRepeat,
  };
  if (truncated.facts || truncated.decisions || truncated.openQuestions || truncated.doNotRepeat) {
    handoff.truncated = truncated;
  }
  if (sensitiveFacts.length || sensitiveDecisions.length) {
    handoff.filteredSensitive = { facts: sensitiveFacts.length, decisions: sensitiveDecisions.length };
  }

  // Final byte-size guard: if still over budget after per-category caps,
  // shed openQuestions/doNotRepeat first, then decisions, then facts —
  // matching Codex's recommended priority "doNotRepeat > openQuestions >
  // decisions > facts" but applied in reverse (we drop the lowest-priority
  // categories first).
  enforceTotalByteCap(handoff);

  return handoff;
}

function enforceTotalByteCap(handoff: MemoryHandoff): void {
  const drop = (category: 'facts' | 'decisions' | 'openQuestions' | 'doNotRepeat'): boolean => {
    const arr = handoff[category];
    if (!Array.isArray(arr) || arr.length === 0) return false;
    arr.pop();
    if (!handoff.truncated) handoff.truncated = { facts: 0, decisions: 0, openQuestions: 0, doNotRepeat: 0 };
    handoff.truncated[category]++;
    return true;
  };
  // Order: drop facts last (Codex priority: facts are the most useful for
  // re-investigation avoidance). doNotRepeat → openQuestions → decisions →
  // facts.
  while (Buffer.byteLength(JSON.stringify(handoff), 'utf-8') > HANDOFF_LIMITS.byteSize) {
    if (drop('doNotRepeat')) continue;
    if (drop('openQuestions')) continue;
    if (drop('decisions')) continue;
    if (drop('facts')) continue;
    break; // can't shrink further
  }
}

const HANDOFF_SCHEMA: AtomicJsonSchema<MemoryHandoff> = {
  expectedVersion: MEMORY_HANDOFF_VERSION,
  validate: (parsed): string | null => {
    const obj = parsed as Record<string, unknown>;
    if (typeof obj.handoffId !== 'string' || obj.handoffId.length === 0) return 'handoffId missing';
    if (typeof obj.parentJobId !== 'string') return 'parentJobId missing';
    if (typeof obj.parentWorkspaceRelative !== 'string') return 'parentWorkspaceRelative missing';
    if (typeof obj.createdAt !== 'string') return 'createdAt missing';
    if (!Array.isArray(obj.facts)) return 'facts must be array';
    if (!Array.isArray(obj.decisions)) return 'decisions must be array';
    if (!Array.isArray(obj.openQuestions)) return 'openQuestions must be array';
    if (!Array.isArray(obj.doNotRepeat)) return 'doNotRepeat must be array';
    return null;
  },
  cast: (parsed): MemoryHandoff => parsed as MemoryHandoff,
};

export function writeHandoffFile(childWorkspaceAbsolute: string, handoff: MemoryHandoff): void {
  const path = join(childWorkspaceAbsolute, MEMORY_HANDOFF_FILE);
  writeAtomicJson(path, handoff);
  logger.info(`[memory-handoff] wrote handoff handoffId=${handoff.handoffId} parentJobId=${handoff.parentJobId} facts=${handoff.facts.length} decisions=${handoff.decisions.length}`);
}

export function readHandoffFile(childWorkspaceAbsolute: string): MemoryHandoff | null {
  const path = join(childWorkspaceAbsolute, MEMORY_HANDOFF_FILE);
  const result = readSafeJson(path, HANDOFF_SCHEMA);
  if (result.kind === 'missing') return null;
  if (result.kind === 'corrupt') {
    logger.warn(`[memory-handoff] corrupt handoff at ${path}: ${result.reason}; skipping`);
    return null;
  }
  return result.value;
}
