/**
 * Phase 5 — child → parent memory delta.
 *
 * When a child subtask completes (success / needs_user_input, or aborted
 * with explicit memory_update), we serialize the **fresh** observations
 * the child accumulated into `<child-workspace>/output/memory-delta.json`.
 * The parent, on resume from `waiting_subtasks`, scans
 * `subtasks/* /output/memory-delta.json` and absorbs each new delta into
 * its own WorkspaceMemory exactly once.
 *
 * Codex review reflection (the 13-point list applied here):
 *   - schema versioned (v1) + atomic JSON write (atomic-json.ts)
 *   - idempotent absorb: every delta has a unique deltaId; the parent
 *     records absorbedDeltaIds and skips re-merge on resume
 *   - corruption tolerance: parse failures and version mismatches log a
 *     warning and skip; absorb continues
 *   - never includes facts whose lineage already references the parent
 *     (those came FROM the parent — emitting them back would loop)
 *   - per-category caps + 256KB total budget, with truncate priority
 *     `doNotRepeat > openQuestions > decisions > facts`
 *   - aborted children only emit a delta if their piece called
 *     memory_update explicitly (`partial: true` flag), otherwise they
 *     stay silent
 */

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_DELTA_FILE = 'output/memory-delta.json';
export const MEMORY_DELTA_VERSION = 1 as const;

export const DELTA_LIMITS = {
  facts: 50,
  decisions: 30,
  openQuestions: 30,
  doNotRepeat: 30,
  byteSize: 256 * 1024,
} as const;

export type ChildPieceStatus = 'success' | 'aborted' | 'needs_user_input';

export interface DeltaFact {
  claim: string;
  confidence: 'high' | 'medium' | 'low';
  evidencePaths: string[];      // child-relative; parent rewrites on absorb
  evidenceUrls: string[];
  observedAt: string;
  portability: Portability;
  evidenceKind: EvidenceKind;
  lineage: LineageEntry[];
}

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

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

export interface SubtaskResultMemoryDelta {
  version: typeof MEMORY_DELTA_VERSION;
  deltaId: string;
  childJobId: string;
  childWorkspaceRelative: string;   // path from parent → child, e.g. "subtasks/1"
  childStatus: ChildPieceStatus;
  partial: boolean;                 // true for aborted-with-explicit-update
  createdAt: string;
  facts: DeltaFact[];
  decisions: DeltaDecision[];
  openQuestions: DeltaOpenQuestion[];
  doNotRepeat: string[];
  truncated?: { facts: number; decisions: number; openQuestions: number; doNotRepeat: number };
}

export interface BuildDeltaInput {
  snapshot: WorkspaceMemorySnapshot;
  childJobId: string;
  childWorkspaceRelative: string;
  childStatus: ChildPieceStatus;
  partial: boolean;
  deltaId: string;
  /** Parent's job ID — used to skip facts that came FROM this parent (avoid loops). */
  parentJobId?: string;
  now?: string;
}

function isInheritedFromParent(entry: { lineage: LineageEntry[] }, parentJobId: string | undefined): boolean {
  if (!parentJobId) return false;
  return entry.lineage.some((e) => e.jobId === parentJobId);
}

function projectFact(f: Fact): DeltaFact {
  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): DeltaDecision {
  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): DeltaOpenQuestion {
  return { question: q.question, createdAt: q.createdAt };
}

/**
 * Build a SubtaskResultMemoryDelta from the child's snapshot.
 *
 *   - Drops facts/decisions whose lineage already references the parent
 *     (= they came FROM the parent's handoff, no value re-emitting them).
 *   - Applies per-category caps, then byte-size cap with priority
 *     `doNotRepeat > openQuestions > decisions > facts` (lowest priority
 *     dropped first; facts are the most useful for re-investigation
 *     avoidance, so they hold last).
 */
export function buildMemoryDelta(input: BuildDeltaInput): SubtaskResultMemoryDelta {
  const freshFacts = input.snapshot.facts.filter((f) => !isInheritedFromParent(f, input.parentJobId));
  const freshDecisions = input.snapshot.decisions.filter((d) => !isInheritedFromParent(d, input.parentJobId));
  const freshOpenQuestions = input.snapshot.openQuestions; // questions don't carry lineage; pass through
  const freshDoNotRepeat = input.snapshot.doNotRepeat;

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

  const cappedFacts = freshFacts.slice(0, DELTA_LIMITS.facts);
  truncated.facts = freshFacts.length - cappedFacts.length;

  const cappedDecisions = freshDecisions.slice(0, DELTA_LIMITS.decisions);
  truncated.decisions = freshDecisions.length - cappedDecisions.length;

  const cappedOpenQuestions = freshOpenQuestions.slice(0, DELTA_LIMITS.openQuestions);
  truncated.openQuestions = freshOpenQuestions.length - cappedOpenQuestions.length;

  const cappedDoNotRepeat = freshDoNotRepeat.slice(0, DELTA_LIMITS.doNotRepeat);
  truncated.doNotRepeat = freshDoNotRepeat.length - cappedDoNotRepeat.length;

  const delta: SubtaskResultMemoryDelta = {
    version: MEMORY_DELTA_VERSION,
    deltaId: input.deltaId,
    childJobId: input.childJobId,
    childWorkspaceRelative: input.childWorkspaceRelative,
    childStatus: input.childStatus,
    partial: input.partial,
    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) {
    delta.truncated = truncated;
  }

  enforceTotalByteCap(delta);

  return delta;
}

function enforceTotalByteCap(delta: SubtaskResultMemoryDelta): void {
  const drop = (category: 'facts' | 'decisions' | 'openQuestions' | 'doNotRepeat'): boolean => {
    const arr = delta[category];
    if (!Array.isArray(arr) || arr.length === 0) return false;
    arr.pop();
    if (!delta.truncated) delta.truncated = { facts: 0, decisions: 0, openQuestions: 0, doNotRepeat: 0 };
    delta.truncated[category]++;
    return true;
  };
  while (Buffer.byteLength(JSON.stringify(delta), 'utf-8') > DELTA_LIMITS.byteSize) {
    if (drop('doNotRepeat')) continue;
    if (drop('openQuestions')) continue;
    if (drop('decisions')) continue;
    if (drop('facts')) continue;
    break;
  }
}

const DELTA_SCHEMA: AtomicJsonSchema<SubtaskResultMemoryDelta> = {
  expectedVersion: MEMORY_DELTA_VERSION,
  validate: (parsed): string | null => {
    const obj = parsed as Record<string, unknown>;
    if (typeof obj.deltaId !== 'string' || obj.deltaId.length === 0) return 'deltaId missing';
    if (typeof obj.childJobId !== 'string') return 'childJobId missing';
    if (typeof obj.childWorkspaceRelative !== 'string') return 'childWorkspaceRelative missing';
    if (typeof obj.childStatus !== 'string') return 'childStatus missing';
    if (typeof obj.partial !== 'boolean') return 'partial must be boolean';
    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): SubtaskResultMemoryDelta => parsed as SubtaskResultMemoryDelta,
};

export function writeDeltaFile(childWorkspaceAbsolute: string, delta: SubtaskResultMemoryDelta): void {
  const path = join(childWorkspaceAbsolute, MEMORY_DELTA_FILE);
  writeAtomicJson(path, delta);
  logger.info(`[memory-delta] wrote delta deltaId=${delta.deltaId} childJobId=${delta.childJobId} status=${delta.childStatus} partial=${delta.partial} facts=${delta.facts.length} decisions=${delta.decisions.length}`);
}

export function readDeltaFile(childWorkspaceAbsolute: string): SubtaskResultMemoryDelta | null {
  const path = join(childWorkspaceAbsolute, MEMORY_DELTA_FILE);
  const result = readSafeJson(path, DELTA_SCHEMA);
  if (result.kind === 'missing') return null;
  if (result.kind === 'corrupt') {
    logger.warn(`[memory-delta] corrupt delta at ${path}: ${result.reason}; skipping`);
    return null;
  }
  return result.value;
}
