/**
 * Part O4 — Incremental subtree recompute.
 *
 * Reuses O2 Substrate.open (caller) — never truncates the ledger.
 * Re-freezes + rebuilds ONLY the edited feature; dependents are re-HEALED by
 * applyRha (parent-recheck / escalate), never silently rebuilt.
 * Unaffected nodes' suite + code stay byte-identical (asserted via freeze hashes
 * and owned-file digests).
 */
import fs from 'node:fs';
import path from 'node:path';
import type { FeatureSpec, JobState, LlmClient } from './types';
import { Substrate } from './substrate';
import { converge } from './bounded';
import { runTsc, runVitest, sha256, toFeatureResult } from './sandbox';
import { raiseSev1 } from './alerts';
import {
  boundaryViolations,
  extractCases,
  readFiles,
  validateTestFile,
  verifyFreeze,
  writeFiles,
  type Emit,
} from './pipeline';
import { applyRha, type ApplyRhaOpts } from './rha';

function fileSha(dir: string, rel: string): string | null {
  const abs = path.join(dir, rel);
  if (!fs.existsSync(abs)) return null;
  return sha256(fs.readFileSync(abs, 'utf8'));
}

function assertUnaffected(
  dir: string,
  snapHashes: Record<string, string>,
  snapCode: Map<string, string>,
  changedTestFile: string,
): void {
  for (const [file, hash] of Object.entries(snapHashes)) {
    if (file === changedTestFile) continue;
    const actual = fileSha(dir, file);
    if (actual !== hash) {
      throw new Error(
        `O4 invariant broken: unaffected suite ${file} changed (${hash.slice(0, 8)}→${(actual ?? 'missing').slice(0, 8)})`,
      );
    }
  }
  for (const [file, hash] of snapCode) {
    const actual = fileSha(dir, file);
    if (actual !== hash) {
      throw new Error(
        `O4 invariant broken: unaffected code ${file} changed (${hash.slice(0, 8)}→${(actual ?? 'missing').slice(0, 8)})`,
      );
    }
  }
}

export interface RecomputeInput {
  featureId: string;
  /** Full FeatureSpec; only `featureId`'s node is applied (others ignored). */
  spec: FeatureSpec;
}

/**
 * Scoped recompute of one feature + RHA heal of its blast radius.
 * Caller must have reopened Substrate via Substrate.open (append-only).
 */
export async function recomputeFeatureSubtree(
  job: JobState,
  substrate: Substrate,
  llm: LlmClient,
  emit: Emit,
  input: RecomputeInput,
  rhaOpts?: ApplyRhaOpts,
): Promise<void> {
  if (!job.spec || !job.manifest) {
    throw new Error('cannot recompute — job has no frozen IR');
  }
  const { featureId } = input;
  const submitted = input.spec.features.find(f => f.id === featureId);
  if (!submitted) {
    throw new Error(`submitted spec has no feature ${featureId}`);
  }
  const prev = job.spec.features.find(f => f.id === featureId);
  if (!prev) {
    throw new Error(`job spec has no feature ${featureId}`);
  }
  if (!job.manifest.features.some(f => f.id === featureId)) {
    throw new Error(`feature ${featureId} was not in the frozen IR (escalated or missing)`);
  }

  const dir = job.workspaceDir;
  const snapHashes = { ...job.manifest.freezeHashes };
  const snapCode = new Map<string, string>();
  for (const f of job.manifest.features) {
    if (f.id === featureId) continue;
    for (const p of f.files) {
      const h = fileSha(dir, p);
      if (h) snapCode.set(p, h);
    }
  }

  // Apply ONLY the edited feature; keep ownership / deps / test path stable.
  const next = {
    ...structuredClone(submitted),
    id: featureId,
    files: [...prev.files],
    testFile: prev.testFile,
    deps: [...prev.deps],
    parentId: prev.parentId,
    stubs: submitted.stubs?.length ? structuredClone(submitted.stubs) : structuredClone(prev.stubs),
  };
  job.spec = {
    ...job.spec,
    title: input.spec.title || job.spec.title,
    features: job.spec.features.map(f => (f.id === featureId ? next : f)),
  };
  fs.writeFileSync(
    path.join(dir, '.supergit', 'spec.json'),
    JSON.stringify(job.spec, null, 2),
  );

  const start = substrate.recordNode({
    type: 'decision',
    featureId,
    status: 'info',
    summary: `incremental recompute of ${featureId}`,
  });
  substrate.commit(start, `recompute(${featureId}): start`);
  emit('recompute', { featureId, phase: 'start' });

  // --- Re-freeze ONLY this feature's suite ---------------------------------
  job.phase = 'generating-ir';
  emit('phase', { phase: job.phase });
  emit('recompute', { featureId, phase: 'refreeze' });

  let { content } = await llm.generateTests(job.spec, next);
  const irOutcome = await converge<string>({
    measure: () => validateTestFile(content),
    signature: ps => ps.join('|'),
    repair: async (ps, pass) => {
      emit('ir-repair', { featureId, pass, findings: ps.length, recompute: true });
      const r = await llm.generateTests(
        job.spec!,
        next,
        `Deterministic gate findings on the previous attempt:\n- ${ps.join('\n- ')}`,
      );
      content = r.content;
    },
  });
  if (irOutcome.errors.length) {
    throw new Error(
      `recompute IR for ${featureId} escalated:\n- ${irOutcome.errors.join('\n- ')}`,
    );
  }

  writeFiles(dir, [{ path: next.testFile, content }]);

  // Suites must still typecheck against existing stubs (no silent stub rewrite).
  let suiteDiags = runTsc(dir).filter(d =>
    d.file.replace(/\\/g, '/').endsWith(next.testFile.replace(/^\.\//, '')),
  );
  for (let pass = 1; pass <= 3 && suiteDiags.length; pass++) {
    emit('consistency-gate', { pass: false, findings: suiteDiags.length, recompute: true, featureId });
    const feedback = suiteDiags
      .map(d => `${d.file}:${d.line} TS${d.code}: ${d.message}`)
      .join('\n');
    const r = await llm.generateTests(job.spec, next, `Typecheck findings:\n${feedback}`);
    if (validateTestFile(r.content).length) continue;
    content = r.content;
    writeFiles(dir, [{ path: next.testFile, content }]);
    suiteDiags = runTsc(dir).filter(d =>
      d.file.replace(/\\/g, '/').endsWith(next.testFile.replace(/^\.\//, '')),
    );
  }
  if (suiteDiags.length) {
    throw new Error(
      `recompute IR for ${featureId} does not typecheck against frozen stubs:\n- `
      + suiteDiags.slice(0, 8).map(d => `${d.file}:${d.line} TS${d.code}: ${d.message}`).join('\n- '),
    );
  }

  const newCases = extractCases(featureId, content);
  const newHash = sha256(content);
  job.manifest = {
    ...job.manifest,
    features: job.manifest.features.map(f => (f.id === featureId ? next : f)),
    cases: [
      ...job.manifest.cases.filter(c => c.featureId !== featureId),
      ...newCases,
    ],
    freezeHashes: {
      ...job.manifest.freezeHashes,
      [next.testFile]: newHash,
    },
    frozenAt: new Date().toISOString(),
  };
  fs.writeFileSync(
    path.join(dir, '.supergit', 'ir-manifest.json'),
    JSON.stringify(job.manifest, null, 2),
  );

  const testNode = substrate.recordNode({
    type: 'test',
    featureId,
    parentId: start,
    causeId: start,
    artifactPath: next.testFile,
    status: 'info',
    summary: `recompute IR suite for ${featureId}: ${newCases.length} cases (new freeze hash)`,
  });
  substrate.commit(testNode, `recompute(${featureId}): re-freeze suite`);
  emit('ir-feature', { featureId, cases: newCases.length, recompute: true });

  // Unaffected suites must still match the pre-recompute freeze hashes.
  assertUnaffected(dir, snapHashes, snapCode, next.testFile);
  verifyFreeze(dir, job.manifest);

  // --- Rebuild ONLY this feature -------------------------------------------
  job.phase = 'building';
  emit('phase', { phase: job.phase });
  emit('recompute', { featureId, phase: 'rebuild' });

  const frozenTest = fs.readFileSync(path.join(dir, next.testFile), 'utf8');
  const depFiles = next.deps.flatMap(d => job.spec!.features.find(x => x.id === d)!.files);
  const depContext = readFiles(dir, depFiles);
  emit('build-feature', { featureId, name: next.name, recompute: true });
  let files = await llm.generateImplementation(job.spec, next, frozenTest, depContext);
  let violations = boundaryViolations(files, next);
  if (violations.length) {
    files = await llm.generateImplementation(job.spec, next, frozenTest, depContext);
    violations = boundaryViolations(files, next);
    if (violations.length) {
      throw new Error(`recompute build for ${featureId} violated write boundary:\n- ${violations.join('\n- ')}`);
    }
  }
  writeFiles(dir, files);

  const codeNode = substrate.recordNode({
    type: 'code',
    featureId,
    parentId: testNode,
    causeId: testNode,
    artifactPath: next.files.join(';'),
    status: 'info',
    summary: `recompute implementation of ${featureId} ${next.name} (${files.length} files)`,
  });
  substrate.commit(codeNode, `recompute(${featureId}): rebuild`);

  const run = runVitest(dir, [next.testFile]);
  const declared = job.manifest.cases.filter(c => c.featureId === featureId).length;
  if (declared > 0 && run.passed + run.failed === 0) {
    raiseSev1(
      job,
      'zero-executed',
      `${next.testFile}: declares ${declared} cases but 0 executed (collection mismatch)`,
      { featureId, testFile: next.testFile, declared, collected: run.collected },
      emit,
    );
  }
  const result = toFeatureResult(featureId, run);
  job.buildResults = [
    ...job.buildResults.filter(r => r.featureId !== featureId),
    result,
  ];
  substrate.recordNode({
    type: 'result',
    featureId,
    parentId: codeNode,
    causeId: codeNode,
    status: run.failed === 0 && run.passed > 0 ? 'pass' : 'fail',
    summary: `recompute authentication: ${run.passed} passed, ${run.failed} failed`,
  });
  emit('auth-result', {
    featureId,
    passed: result.passed,
    failed: result.failed,
    failures: result.failures.slice(0, 5),
    recompute: true,
  });

  assertUnaffected(dir, snapHashes, snapCode, next.testFile);

  // --- Dependents: RHA heal / escalate — never rebuild ---------------------
  job.phase = 'healing';
  emit('phase', { phase: job.phase });
  emit('recompute', { featureId, phase: 'heal' });
  const blast = job.spec.features.filter(f => f.deps.includes(featureId)).map(f => f.id);
  const healNote = substrate.recordNode({
    type: 'decision',
    featureId,
    status: 'info',
    summary: `recompute blast radius: dependents [${blast.join(', ') || 'none'}] will be re-healed by RHA (not rebuilt)`,
  });
  substrate.commit(healNote, `recompute(${featureId}): heal dependents via RHA`);

  const report = await applyRha(
    dir,
    job.spec,
    job.manifest,
    substrate,
    llm,
    emit,
    job,
    rhaOpts,
  );
  job.report = report;
  job.phase = report.failed === 0 ? 'complete' : 'failed';
  if (report.failed > 0) {
    job.error = `recompute left ${report.failed} failing test(s) — escalated for review`;
  } else {
    job.error = null;
  }

  assertUnaffected(dir, snapHashes, snapCode, next.testFile);
  verifyFreeze(dir, job.manifest);

  const done = substrate.recordNode({
    type: 'decision',
    featureId,
    status: report.failed === 0 ? 'pass' : 'fail',
    summary: `incremental recompute of ${featureId} complete — ${report.failed === 0 ? 'green' : report.failed + ' failing'}`,
  });
  substrate.commit(done, `recompute(${featureId}): done`);
  emit('recompute', { featureId, phase: 'done', failed: report.failed });
  emit('phase', { phase: job.phase });
  emit('complete', { report: report as unknown as Record<string, unknown>, recompute: true });
}
