/**
 * Part U2 — add features to an existing frozen project (iterate-in-place).
 * Appends new nodes only; existing freeze hashes + code stay byte-identical.
 */
import fs from 'node:fs';
import path from 'node:path';
import type { FeatureNode, 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,
  topoOrder,
  validateTestFile,
  verifyFreeze,
  writeFiles,
  type Emit,
} from './pipeline';
import { applyRha, type ApplyRhaOpts } from './rha';
import { checkScaffoldingIntegrity } from './scaffoldGuard';

function nextFeatureIds(existing: string[], n: number): string[] {
  let max = 0;
  for (const id of existing) {
    const m = /^F(\d+)/i.exec(id);
    if (m) max = Math.max(max, Number(m[1]));
  }
  return Array.from({ length: n }, (_, i) => `F${max + 1 + i}`);
}

function assertOldIntact(
  dir: string,
  snapHashes: Record<string, string>,
  snapCode: Map<string, string>,
): void {
  for (const [file, hash] of Object.entries(snapHashes)) {
    const abs = path.join(dir, file);
    const actual = fs.existsSync(abs) ? sha256(fs.readFileSync(abs, 'utf8')) : '(missing)';
    if (actual !== hash) {
      throw new Error(`add-features invariant: existing suite ${file} changed`);
    }
  }
  for (const [file, hash] of snapCode) {
    const abs = path.join(dir, file);
    const actual = fs.existsSync(abs) ? sha256(fs.readFileSync(abs, 'utf8')) : '(missing)';
    if (actual !== hash) {
      throw new Error(`add-features invariant: existing code ${file} changed`);
    }
  }
}

/**
 * Encode ONLY new features from `addPrompt`, append to the frozen tree,
 * freeze+build the newcomers, then RHA-heal (existing nodes untouched).
 */
export async function addFeaturesToProject(
  job: JobState,
  substrate: Substrate,
  llm: LlmClient,
  emit: Emit,
  addPrompt: string,
  rhaOpts?: ApplyRhaOpts,
): Promise<void> {
  if (!job.spec || !job.manifest) {
    throw new Error('cannot add features — job has no frozen IR');
  }
  const guard = checkScaffoldingIntegrity(job.workspaceDir, job.manifest);
  if (!guard.ok) {
    throw new Error(guard.message ?? 'scaffolding integrity check failed');
  }

  const dir = job.workspaceDir;
  const snapHashes = { ...job.manifest.freezeHashes };
  const snapCode = new Map<string, string>();
  for (const f of job.manifest.features) {
    for (const p of f.files) {
      const abs = path.join(dir, p);
      if (fs.existsSync(abs)) snapCode.set(p, sha256(fs.readFileSync(abs, 'utf8')));
    }
  }

  const start = substrate.recordNode({
    type: 'decision',
    status: 'info',
    summary: `iterate-in-place: add features from prompt (${addPrompt.slice(0, 80)}…)`,
  });
  substrate.commit(start, 'add-features: start');
  emit('add-features', { phase: 'start' });

  job.phase = 'encoding';
  emit('phase', { phase: job.phase });

  // Ask the model for features of the ADDITION only; remap ids onto the next free Fn.
  const encoded = await llm.encodeFeatures(
    `Existing frozen features (DO NOT regenerate these — only encode NEW capabilities):\n`
    + job.spec.features.map(f => `- ${f.id}: ${f.name}`).join('\n')
    + `\n\nNew capabilities to ADD to this project:\n${addPrompt}`,
  );
  if (!encoded.features.length) {
    throw new Error('add-features: model returned zero new features');
  }

  const ids = nextFeatureIds(job.spec.features.map(f => f.id), encoded.features.length);
  const newcomers: FeatureNode[] = encoded.features.map((f, i) => {
    const id = ids[i];
    // Remap any deps that pointed at temporary ids onto prior project features by name when possible.
    const deps = f.deps
      .map(d => {
        const byOld = encoded.features.findIndex(x => x.id === d);
        if (byOld >= 0 && byOld < i) return ids[byOld];
        if (job.spec!.features.some(x => x.id === d)) return d;
        return null;
      })
      .filter((x): x is string => !!x);
    return {
      ...f,
      id,
      deps,
      parentId: f.parentId && ids.includes(f.parentId) ? f.parentId : null,
      testFile: f.testFile?.startsWith('tests/') ? f.testFile : `tests/${id.toLowerCase()}.test.ts`,
      files: (f.files?.length ? f.files : [`src/${id.toLowerCase()}.ts`]).map(p =>
        p.startsWith('src/') ? p : `src/${p}`,
      ),
      stubs: (f.stubs?.length ? f.stubs : []).map(s => ({
        ...s,
        path: s.path.startsWith('src/') ? s.path : `src/${s.path}`,
      })),
    };
  });

  // Ensure stubs exist for owned files
  for (const f of newcomers) {
    if (!f.stubs.length) {
      f.stubs = f.files.map(p => ({
        path: p,
        content: `export function _stub() { throw new Error('stub: not implemented'); }\n`,
      }));
    }
    writeFiles(dir, f.stubs);
  }

  job.spec = {
    ...job.spec,
    features: [...job.spec.features, ...newcomers],
  };
  fs.writeFileSync(path.join(dir, '.supergit', 'spec.json'), JSON.stringify(job.spec, null, 2));

  for (const f of newcomers) {
    const n = substrate.recordNode({
      type: 'spec', featureId: f.id, parentId: start, status: 'info',
      summary: `added feature ${f.id} ${f.name}`,
    });
    substrate.commit(n, `add(${f.id}): ${f.name}`);
  }

  // --- IR for newcomers only -----------------------------------------------
  job.phase = 'generating-ir';
  emit('phase', { phase: job.phase });

  const newCases = [...job.manifest.cases];
  const freezeHashes = { ...job.manifest.freezeHashes };
  const survivorNodes = [...job.manifest.features];

  for (const f of topoOrder(newcomers)) {
    let { content } = await llm.generateTests(job.spec, f);
    const irOutcome = await converge<string>({
      measure: () => validateTestFile(content),
      signature: ps => ps.join('|'),
      repair: async (ps, pass) => {
        emit('ir-repair', { featureId: f.id, pass, findings: ps.length, add: true });
        const r = await llm.generateTests(
          job.spec!,
          f,
          `Deterministic gate findings:\n- ${ps.join('\n- ')}`,
        );
        content = r.content;
      },
    });
    if (irOutcome.errors.length) {
      throw new Error(`add-features IR escalated for ${f.id}:\n- ${irOutcome.errors.join('\n- ')}`);
    }
    writeFiles(dir, [{ path: f.testFile, content }]);
    const suiteDiags = runTsc(dir).filter(d =>
      d.file.replace(/\\/g, '/').endsWith(f.testFile.replace(/^\.\//, '')),
    );
    if (suiteDiags.length) {
      throw new Error(
        `add-features IR for ${f.id} does not typecheck:\n- `
        + suiteDiags.slice(0, 6).map(d => `${d.file}:${d.line} TS${d.code}`).join('\n- '),
      );
    }
    const cases = extractCases(f.id, content);
    newCases.push(...cases);
    freezeHashes[f.testFile] = sha256(content);
    survivorNodes.push(f);
    const testNode = substrate.recordNode({
      type: 'test', featureId: f.id, status: 'info',
      artifactPath: f.testFile,
      summary: `IR suite for added ${f.id}: ${cases.length} cases`,
    });
    substrate.commit(testNode, `add(${f.id}): freeze suite`);
    emit('ir-feature', { featureId: f.id, cases: cases.length, add: true });
  }

  job.manifest = {
    ...job.manifest,
    features: survivorNodes,
    cases: newCases,
    freezeHashes,
    frozenAt: new Date().toISOString(),
  };
  fs.writeFileSync(path.join(dir, '.supergit', 'ir-manifest.json'), JSON.stringify(job.manifest, null, 2));
  assertOldIntact(dir, snapHashes, snapCode);
  verifyFreeze(dir, job.manifest);

  // --- Build newcomers only ------------------------------------------------
  job.phase = 'building';
  emit('phase', { phase: job.phase });
  for (const f of topoOrder(newcomers)) {
    const frozenTest = fs.readFileSync(path.join(dir, f.testFile), 'utf8');
    const depFiles = f.deps.flatMap(d => job.spec!.features.find(x => x.id === d)!.files);
    const depContext = readFiles(dir, depFiles);
    emit('build-feature', { featureId: f.id, name: f.name, add: true });
    let files = await llm.generateImplementation(job.spec, f, frozenTest, depContext);
    let violations = boundaryViolations(files, f);
    if (violations.length) {
      files = await llm.generateImplementation(job.spec, f, frozenTest, depContext);
      violations = boundaryViolations(files, f);
      if (violations.length) {
        throw new Error(`add-features build for ${f.id} violated write boundary`);
      }
    }
    writeFiles(dir, files);
    const codeNode = substrate.recordNode({
      type: 'code', featureId: f.id, status: 'info',
      artifactPath: f.files.join(';'),
      summary: `implementation of added ${f.id} ${f.name}`,
    });
    substrate.commit(codeNode, `add(${f.id}): implement`);
    const run = runVitest(dir, [f.testFile]);
    const declared = job.manifest.cases.filter(c => c.featureId === f.id).length;
    if (declared > 0 && run.passed + run.failed === 0) {
      raiseSev1(job, 'zero-executed', `${f.testFile}: 0 executed`, { featureId: f.id }, emit);
    }
    const result = toFeatureResult(f.id, run);
    job.buildResults = [...job.buildResults.filter(r => r.featureId !== f.id), result];
    emit('auth-result', { featureId: f.id, passed: result.passed, failed: result.failed, add: true });
  }

  assertOldIntact(dir, snapHashes, snapCode);

  job.phase = 'healing';
  emit('phase', { phase: job.phase });
  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 = `add-features left ${report.failed} failing test(s)`;
  } else {
    job.error = null;
  }

  assertOldIntact(dir, snapHashes, snapCode);
  const done = substrate.recordNode({
    type: 'decision',
    status: report.failed === 0 ? 'pass' : 'fail',
    summary: `add-features complete: added ${newcomers.map(f => f.id).join(', ')}`,
  });
  substrate.commit(done, 'add-features: done');
  emit('add-features', { phase: 'done', added: newcomers.map(f => f.id), failed: report.failed });
  emit('phase', { phase: job.phase });
  emit('complete', { report: report as unknown as Record<string, unknown>, add: true });
}

export type { FeatureSpec };
