'use client';

import { useState, useEffect, useRef, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
  CheckCircle2, AlertTriangle, Info, ChevronRight, ChevronLeft,
  Download, Share2, Eye, FileText, Play,
  Activity, Zap, Shield, Database, Code2, GitBranch, Clock,
  TrendingUp, Layers, Target, Settings, ArrowRight, Loader2,
  Check, X, History, GitCompare, Save, ExternalLink, Search,
  Server, Workflow, FolderPlus, Users, Calendar, Tag, Bell,
  BarChart3, PieChart, LineChart, Globe, Lock, Key, Terminal,
  FileCode, Cpu, HardDrive, Network, Cloud, Rocket, Gauge,
  Flame, Sparkles, Award, Trophy, Medal, Star, Heart,
  Container, RefreshCw, Bug
} from 'lucide-react';
import { api } from '../lib/api';
import { useSession } from '../lib/session-context';
import type { Project } from '../data/projectsData';
import {
  categorizeEngineLogMessage,
  classifyConversionFailure,
  sanitizeEngineLogForDisplay,
  buildMigrationDecisionPaths,
  isAccountingSnapshot,
  labelAccountingStage,
  inferPrimaryFailureFromLogs,
  isStructuralFailureEngineLogLine,
  categorizeConversionErrors,
  type EngineLogCategory,
} from '../lib/engine-integration';
import { derivePerformanceMetricsFromRun, getLangBaseline } from '../lib/perf-metrics';
import { getRuleLabel, getRuleSeverityClass, categoryOf, type RuleCategory } from '../lib/rule-labels';
import { normalizeProjectLang, buildScribaDeployWorkflow } from '../lib/cicd-workflow';
import { PIPELINE_PLUGINS, PLUGIN_IDS, getEnabledPluginSet, isPluginEnabled } from '../lib/pipeline-plugins';
import { buildStartRunOptions, needsLegacyConvert, mergeRules, type RunUsageSnapshot } from '../lib/platform-run';
import CostApprovalGate from './CostApprovalGate';
import ReviewBoard from './ReviewBoard';

type MigrationPhase = 'pre-analysis' | 'validation' | 'pre-flight' | 'migration' | 'verification' | 'completed' | 'failed';

interface MigrationStep {
  id: string;
  name: string;
  description: string;
  status: 'pending' | 'running' | 'completed' | 'skipped' | 'failed';
  duration?: string;
  metrics?: Record<string, number>;
  details?: string[];
}

interface ArtifactModuleMeta {
  index: number;
  sourceRel: string;
  targetRel: string;
  sourceLeaf: string;
  targetLeaf: string;
  linesSource: number;
  linesTarget: number;
}

function normalizeEnginePath(p: string | undefined): string {
  if (!p || typeof p !== 'string') return '';
  return p.replace(/^\/+/u, '').replace(/\\/gu, '/').trim();
}

/** Normalize `project.config` from API/DB (object or occasional JSON string). */
function parseProjectConfig(project: unknown): Record<string, unknown> {
  const raw = (project as Record<string, unknown> | null | undefined)?.config;
  if (raw == null) return {};
  if (typeof raw === 'string') {
    try {
      const parsed: unknown = JSON.parse(raw);
      if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
        return parsed as Record<string, unknown>;
      }
    } catch {
      /* ignore */
    }
    return {};
  }
  if (typeof raw === 'object' && !Array.isArray(raw)) return raw as Record<string, unknown>;
  return {};
}

function extractCustomRules(cfg: Record<string, unknown>): string | undefined {
  const v = cfg.customRules;
  if (typeof v === 'string') {
    const t = v.trim();
    return t.length > 0 ? t : undefined;
  }
  if (Array.isArray(v)) {
    const t = v.map((x) => String(x)).join('\n').trim();
    return t.length > 0 ? t : undefined;
  }
  return undefined;
}

/** Queue rows in `notification_outbox` when wizard notification prefs allow (non-blocking). */
function enqueueMigrationNotify(projectId: string, event: 'complete' | 'warning' | 'failure', detail?: string) {
  void api.enqueueMigrationNotifications(projectId, { event, detail }).catch(() => {});
}

function artifactModuleMeta(
  ef: { sourcePath?: string; outputPath?: string },
  idx: number,
  linesSource: number,
  linesTarget: number,
): ArtifactModuleMeta {
  const targetRel = normalizeEnginePath(ef.outputPath);
  const sourceRel = normalizeEnginePath(ef.sourcePath);
  const targetLeaf = targetRel.split('/').pop() || `target-${idx}`;
  const rawSrc = sourceRel.split('/').pop() || '';
  const sourceLeaf = rawSrc.length > 0 ? rawSrc : targetLeaf.replace(/\.[^.]+$/, '') || `source-${idx}`;
  return {
    index: idx,
    sourceRel,
    targetRel,
    sourceLeaf,
    targetLeaf,
    linesSource,
    linesTarget,
  };
}

function disambiguateTranslationFilePaths(files: { sourceFile: string; targetFile: string }[]): void {
  const seen = new Map<string, number>();
  for (const f of files) {
    const pair = `${f.sourceFile}\0${f.targetFile}`;
    const n = (seen.get(pair) ?? 0) + 1;
    seen.set(pair, n);
    if (n <= 1) continue;
    const dot = f.sourceFile.lastIndexOf('.');
    const ins = dot > 0 ? `${f.sourceFile.slice(0, dot)}__m${n}${f.sourceFile.slice(dot)}` : `${f.sourceFile}__m${n}`;
    f.sourceFile = ins;
  }
}

type EngineGeneratedTestFile = {
  path?: string;
  outputPath?: string;
  content: string;
  testCount?: number;
};

function engineTestFilePath(f: EngineGeneratedTestFile): string {
  const p = (f.path ?? f.outputPath ?? '').trim();
  return p || 'unknown-test-file';
}

function langIdForTarget(tgtLang: string): string {
  return tgtLang === 'typescript' || tgtLang === 'javascript' || tgtLang === 'node' ? 'typescript' : tgtLang;
}

function mapEngineTestsToArtifacts(
  files: EngineGeneratedTestFile[],
  kind: 'unit' | 'integration',
  langId: string,
  idPrefix: string,
): ToolingArtifactFile[] {
  return files.map((f, i) => {
    const filePath = engineTestFilePath(f);
    const name = filePath.split('/').pop() ?? filePath;
    return {
      id: `${idPrefix}${i}`,
      name,
      path: filePath,
      lang: langId,
      lines: Math.max(1, f.content.split('\n').length),
      status: 'info' as const,
      description: kind === 'unit' ? `Unit · ${name}` : `Integration · ${name}`,
      code: f.content,
    };
  });
}

function sumTestCases(files: EngineGeneratedTestFile[]): number {
  return files.reduce((sum, f) => sum + (typeof f.testCount === 'number' && f.testCount > 0 ? f.testCount : 0), 0);
}

function serviceNameFromEngineFile(ef: { outputPath?: string }, i: number): string {
  const p = typeof ef.outputPath === 'string' ? ef.outputPath : '';
  return p.split('/').pop()?.replace(/\.[^/.]+$/, '') ?? `Service${i}`;
}

function langDisplayName(id: string): string {
  const names: Record<string, string> = {
    cobol: 'COBOL', java: 'Java', typescript: 'TypeScript', javascript: 'JavaScript',
    python: 'Python', csharp: 'C#', rpg: 'RPG', pascal: 'Pascal', pli: 'PL/I',
    kotlin: 'Kotlin', go: 'Go', rust: 'Rust', cpp: 'C++', c: 'C',
    tibco_bw: 'TIBCO BusinessWorks',
  };
  return names[id.toLowerCase()] ?? id;
}

function deriveExplanations(args: {
  srcLang: string; tgtLang: string;
  linesSource: number; linesTarget: number;
  accuracy: number; iterations: number;
  quality?: { qualityIndex: number; level: string; buildReadiness?: number; semanticFidelity?: number; idiomaticity?: number } | null;
  warnings: string[];
}): string[] {
  const { srcLang, tgtLang, linesSource, linesTarget, accuracy, iterations, quality, warnings } = args;
  const src = langDisplayName(srcLang);
  const tgt = langDisplayName(tgtLang);
  const out: string[] = [];

  const sizeDelta = linesSource > 0 ? Math.round((linesTarget - linesSource) / linesSource * 100) : 0;
  const sizeDir = sizeDelta >= 0 ? `+${sizeDelta}` : `${sizeDelta}`;
  out.push(
    `Converted ${linesSource.toLocaleString()} lines of ${src} to ${linesTarget.toLocaleString()} lines of ${tgt} (${sizeDir}% size change). ` +
    (sizeDelta > 0
      ? `The increase is typical when migrating from a concise legacy language to a more explicit, type-safe target.`
      : sizeDelta < 0
        ? `The reduction reflects more expressive target-language constructs replacing verbose legacy patterns.`
        : `The line count stayed roughly equal, indicating a close structural mapping between source and target.`)
  );

  const accLabel = accuracy >= 95 ? 'excellent' : accuracy >= 85 ? 'high' : accuracy >= 70 ? 'acceptable' : 'partial';
  out.push(
    `Parity score: ${accuracy}% (${accLabel}). The validator measured ${accuracy}% behavioral equivalence between the source and translated output across all covered execution paths.`
  );

  if (quality != null) {
    const parts: string[] = [];
    if (typeof quality.buildReadiness === 'number') parts.push(`compile readiness ${quality.buildReadiness}%`);
    if (typeof quality.semanticFidelity === 'number') parts.push(`semantic fidelity ${quality.semanticFidelity}%`);
    if (typeof quality.idiomaticity === 'number') parts.push(`idiomaticity ${quality.idiomaticity}%`);
    const detail = parts.length > 0 ? ` — ${parts.join(', ')}` : '';
    out.push(`Quality index: ${quality.qualityIndex} (${quality.level})${detail}.`);
  }

  if (iterations > 1) {
    out.push(
      `Completed in ${iterations} refinement iteration${iterations > 1 ? 's' : ''}. The engine ran additional passes to repair compile errors and improve semantic fidelity before producing the final output.`
    );
  } else {
    out.push(`Completed in a single pass — no repair iterations were necessary, indicating a straightforward structural mapping between the two languages.`);
  }

  if (warnings.length > 0) {
    out.push(`${warnings.length} warning${warnings.length > 1 ? 's' : ''} noted during conversion. Review the Artifacts → Engine Report tab for the full diagnostics list.`);
  }

  return out;
}

function deriveReasonings(args: {
  srcLang: string; tgtLang: string;
  businessRules: string[];
  quality?: { qualityIndex: number; level: string; markerApplied?: boolean } | null;
  accuracy: number;
}): string[] {
  const { srcLang, tgtLang, businessRules, quality, accuracy } = args;
  const src = langDisplayName(srcLang);
  const tgt = langDisplayName(tgtLang);
  const out: string[] = [];

  const pairKey = `${srcLang.toLowerCase()}→${tgtLang.toLowerCase()}`;
  const pairReason: Record<string, string> = {
    'cobol→java': `COBOL divisions were mapped to a Java class: WORKING-STORAGE variables became typed instance fields, PERFORM statements became method calls, and EVALUATE conditions became switch expressions. BigDecimal was used for COMP-3 / PACKED-DECIMAL fields to preserve fixed-point decimal precision.`,
    'cobol→typescript': `COBOL data structures were modeled as TypeScript interfaces. WORKING-STORAGE items became typed properties; PERFORM/VARYING loops became for-of loops. Strict typing was applied throughout to make implicit COBOL data contracts explicit at compile time.`,
    'cobol→python': `COBOL paragraphs were translated to Python functions grouped in a module. The flat record structure was mapped to dataclasses. PERFORM VARYING loops became for-loops; EVALUATE became match-case (Python 3.10+). Decimal arithmetic uses the built-in decimal module.`,
    'java→typescript': `Java classes were mapped to TypeScript classes with equivalent visibility modifiers. Generics were preserved; checked exceptions became typed error unions or Result types. Java Stream pipelines were translated to Array methods (map, filter, reduce).`,
    'java→python': `Java classes became Python dataclasses or plain classes. Static typing was preserved via type hints. Java generics map to Generic[T] and TypeVar. Spring annotations were noted as TODOs for manual review.`,
    'rpg→java': `RPG programs were restructured as Java service classes. /FREE subroutines became methods; data structures became POJOs. File I/O operations were replaced with repository-pattern stubs for JPA integration.`,
    'rpg→typescript': `RPG modules were mapped to TypeScript service classes with async methods. Data structures became interfaces; indicator variables became boolean flags. File operations were replaced with repository stubs.`,
    'pascal→java': `Pascal procedures and functions were mapped to Java static or instance methods. Pascal records became Java POJOs; unit structure became a Java class with a static entry point.`,
    'pli→java': `PL/I procedures became Java methods. PL/I STRUCTURE maps to nested Java classes; ON conditions became try-catch blocks. Fixed-point arithmetic uses BigDecimal.`,
  };

  out.push(
    pairReason[pairKey] ??
    `${src} constructs were mapped to idiomatic ${tgt} equivalents. The engine applied language-specific rules to translate control flow, data structures, and I/O patterns while preserving the original business logic.`
  );

  if (businessRules.length > 0) {
    const shown = businessRules.slice(0, 3).map(r => `"${r}"`).join('; ');
    const tail = businessRules.length > 3 ? ` and ${businessRules.length - 3} more` : '';
    out.push(`${businessRules.length} business rule${businessRules.length > 1 ? 's' : ''} influenced the translation: ${shown}${tail}.`);
  }

  if (quality?.markerApplied) {
    out.push(`The output was tagged with the @scriba-ai-generated marker (quality index ${quality.qualityIndex} ≥ threshold), certifying it as AI-translated code that passed the minimum fitness check for downstream compliance.`);
  }

  if (accuracy < 85) {
    out.push(`The parity score of ${accuracy}% suggests some behavioral differences remain. Manual review is recommended for any sections flagged with TODO markers in the output.`);
  }

  return out;
}

type AnalysisReportShape = {
  files?: { path: string; language: string; lines: number; role: string }[];
  dependencies?: { from: string; to: string; type: string }[];
  dataStructures?: { name: string; type: string; fields: string[] }[];
  businessRules?: { description: string; location: string }[];
  complexity?: { totalLines: number; avgComplexity: number };
  warnings?: string[];
};

function docMarkdownForModule(
  serviceName: string,
  srcLang: string,
  tgtLang: string,
  sourceHint: string,
  targetHint: string,
  meta?: ArtifactModuleMeta,
  report?: AnalysisReportShape,
): string {
  const src = sourceHint || '—';
  const tgt = targetHint || '—';
  const srcLangLabel = srcLang.toUpperCase();
  const tgtLangLabel = tgtLang.toUpperCase();

  // Conversion metrics table rows
  const metricsRows = meta ? [
    `| Source file | \`${meta.sourceRel || src}\` |`,
    `| Target file | \`${meta.targetRel || tgt}\` |`,
    `| Source lines | ${meta.linesSource.toLocaleString()} |`,
    `| Target lines | ${meta.linesTarget.toLocaleString()} |`,
    `| Conversion slot | ${meta.index + 1} |`,
    `| Source language | ${srcLangLabel} |`,
    `| Target language | ${tgtLangLabel} |`,
  ] : [`| Source | \`${src}\` |`, `| Target | \`${tgt}\` |`];

  // File inventory section
  const fileRows = (report?.files ?? []).map(
    (f) => `| \`${f.path}\` | ${f.language.toUpperCase()} | ${f.lines.toLocaleString()} | ${f.role} |`,
  );
  const fileSection = fileRows.length > 0
    ? `## Source File Inventory\n\n| File | Language | Lines | Role |\n| --- | --- | ---: | --- |\n${fileRows.join('\n')}\n\n`
    : '';

  // Data structures section
  const dsBlocks = (report?.dataStructures ?? []).map((ds) => {
    const fieldList = ds.fields.length > 0
      ? ds.fields.map((f) => `  - \`${f}\``).join('\n')
      : '  - *(no fields extracted)*';
    return `### \`${ds.name}\` *(${ds.type})*\n\n${fieldList}`;
  });
  const dsSection = dsBlocks.length > 0
    ? `## Data Structures\n\n${dsBlocks.join('\n\n')}\n\n`
    : '';

  // Dependencies section
  const depRows = (report?.dependencies ?? []).map(
    (d) => `| \`${d.from}\` | \`${d.to}\` | ${d.type} |`,
  );
  const depSection = depRows.length > 0
    ? `## Dependencies\n\n| From | To | Type |\n| --- | --- | --- |\n${depRows.join('\n')}\n\n`
    : '';

  // Business rules section
  const ruleItems = (report?.businessRules ?? []).map(
    (r) => `- **${r.description}**${r.location ? ` *(${r.location})*` : ''}`,
  );
  const rulesSection = ruleItems.length > 0
    ? `## Business Rules\n\nThe following rules were extracted from the source and must be preserved exactly:\n\n${ruleItems.join('\n')}\n\n`
    : '';

  // Complexity section
  const cx = report?.complexity;
  const complexitySection = cx
    ? `## Complexity\n\n| Metric | Value |\n| --- | ---: |\n| Total lines | ${cx.totalLines.toLocaleString()} |\n| Avg cyclomatic complexity | ${cx.avgComplexity} |\n\n`
    : '';

  // Warnings section
  const warnItems = (report?.warnings ?? []).filter(Boolean).map((w) => `- ⚠️ ${w}`);
  const warningsSection = warnItems.length > 0
    ? `## Migration Warnings\n\nFlagged by the analyzer — requires manual review:\n\n${warnItems.join('\n')}\n\n`
    : '';

  // Language-specific migration notes
  const lang = srcLang.toLowerCase();
  let migrationNotes = '';
  if (lang === 'rpg' || lang === 'rpg_free') {
    migrationNotes = `## RPG Migration Notes

- **Fixed-format specs** (H/F/D/I/C) rewritten as free-format \`ctl-opt\`, \`dcl-f\`, \`dcl-s\`, \`dcl-ds\`, \`dcl-pi\`.
- **IP/UP primary cycles** replaced with explicit \`read / dow not %eof / enddo\` loops.
- **Level-break indicators** (L1–L9) replaced with \`dcl-s prev_key like(keyField)\` and explicit key comparisons.
- **Numeric indicators** (*in41, *inlr…) replaced with named \`ind\` variables.
- **GOTO/TAG** replaced with structured \`if\` / \`leave\` / \`iter\` / \`dow\`.
- **MOVEL/MOVE** replaced with direct assignment or \`%subst\` as appropriate.
- **EVAL(H)** replaced with \`%dech(expr : digits : dec)\`.
- **SUBDUR/ADDDUR** replaced with date arithmetic (\`+= %days(n)\`, \`%diff\`).
- **KLIST/KFLD** replaced with parenthesised key lists in CHAIN/SETLL/READE.
- **CALL/PARM** replaced with \`callp PROG(p1 : p2)\` and \`dcl-pr\` / \`dcl-pi\` pairs.
- **OPNQRYF coupling**: if a CL caller pre-sorts/filters via OPNQRYF, the key list in SETLL/READE must match that sort order.

`;
  } else if (lang === 'cobol') {
    migrationNotes = `## COBOL Migration Notes

- **WORKING-STORAGE** fields mapped to instance fields (camelCase in OOP targets).
- **PIC S9(n)V9(m) COMP-3** → \`BigDecimal\` / \`bcmath\` / decimal — never float for money.
- **88-level conditions** converted to boolean helpers, constants, or enum variants.
- **PERFORM THRU** and fall-through paragraphs rewritten as structured methods.
- **COPY members** treated as shared record layouts; confirm all copybook field names resolve.
- **EXEC SQL** blocks preserved or mapped to target ORM/repository pattern.

`;
  } else if (lang === 'tibco_bw') {
    migrationNotes = `## TIBCO BusinessWorks Migration Notes

- **Process definitions** (.process files) mapped to service classes or workflow handlers in the target.
- **Shared Variables / Job Shared Variables** converted to context objects, request-scoped beans, or thread-safe singletons.
- **TIBCO EMS topics/queues** replaced with the target messaging system (Kafka, RabbitMQ, JMS, etc.).
- **Adapters** (JDBC, File, HTTP, SOAP) migrated to equivalent client libraries or integration connectors.
- **Fault handlers** and **error transitions** must be explicitly re-implemented — no implicit retry by default.
- Verify checkpoint/recovery semantics if the source used TIBCO BW's built-in restart/recovery.

`;
  } else if (lang === 'jcl') {
    migrationNotes = `## JCL Migration Notes

- **EXEC PGM=/PROC=** steps mapped to equivalent pipeline or workflow steps.
- **DD statements** converted to file path references or storage bindings.
- **COND= parameters** translated to conditional step execution in the target workflow.
- Verify step dependency order is preserved in the generated output.

`;
  } else {
    migrationNotes = `## Migration Notes

- Verify all public APIs, exported symbols, and I/O contracts are functionally equivalent to the source.
- Rounding, truncation, and numeric precision: confirm against language-native decimal types.
- Error handling paths from the source must have explicit counterparts — do not assume defaults.

`;
  }

  // Test strategy
  const testExt = ['java', 'kotlin'].includes(tgtLang) ? 'java'
    : tgtLang === 'python' ? 'py'
    : tgtLang === 'php' ? 'php'
    : tgtLang === 'csharp' ? 'cs'
    : 'ts';

  // Known gaps checklist
  const langGaps = (lang === 'rpg' || lang === 'rpg_free') ? [
    'Verify OPNQRYF coupling — does any CL caller impose a sort/filter this program depends on?',
    'Confirm level-break flush after the final `enddo` handles the last group correctly.',
    'Check hard-coded dates flagged in warnings — valid business rules or dead-code bypasses?',
    'Confirm all `ind` variables replacing numeric indicators have correct *on/*off semantics.',
  ] : lang === 'cobol' ? [
    'Confirm all 88-level conditions map to the correct enum/constant values.',
    'Validate MOVE/REDEFINES boundary semantics with production data samples.',
    'Verify EXEC SQL cursors close under all exit paths (GOBACK, early return).',
  ] : lang === 'tibco_bw' ? [
    'Verify all TIBCO EMS destinations (topics/queues) are mapped to the correct target broker.',
    'Confirm fault handler and error transition logic is fully re-implemented in the target.',
    'Validate that Shared Variable / Job Shared Variable scoping is preserved in the migrated service.',
  ] : [
    'Confirm all error paths from the source have explicit counterparts in the generated code.',
    'Validate numeric precision and rounding against production data samples.',
  ];

  const gapItems = [
    ...langGaps,
    'Review any `// TODO` or incomplete markers left in the generated output.',
    'Run the full test suite against a representative data sample before merging.',
  ].map((g) => `- [ ] ${g}`).join('\n');

  return `# ${serviceName}

> **Migration:** \`${src}\` (${srcLangLabel}) → \`${tgt}\` (${tgtLangLabel})
> Generated by Scriba · ${new Date().toISOString().slice(0, 10)}

## Conversion Metrics

| Metric | Value |
| --- | --- |
${metricsRows.join('\n')}

${fileSection}${dsSection}${depSection}${rulesSection}${complexitySection}${warningsSection}${migrationNotes}## Test Strategy

- **Unit** — \`tests/unit/${serviceName}Test.${testExt}\`: boundary values, parity checks against legacy input/output samples.
- **Integration** — End-to-end or file-replay tests covering the full job chain if this module participates in one.
- **Regression / golden files** — snapshot tests for stable fixed-width or structured outputs (reports, extracts).
- **Business rule parity** — for each rule listed above, write at least one test verifying the migrated code produces the same result as the original.

## Known Gaps and Follow-ups

${gapItems}

## References

- Project Artifacts tab — AI-generated unit and integration tests for this module.
- Runbook — add links to ticketing system, on-call rotation, and rollback procedure.

---
*Review all sections before production sign-off.*
`;
}

interface ToolingArtifactFile {
  id: string;
  name: string;
  path: string;
  lang: string;
  lines: number;
  status: 'info';
  description: string;
  code: string;
}

function wizardEnabled(
  cfg: Record<string, unknown>,
  key: string,
  defaultIfUnset: boolean,
): boolean {
  const v = cfg[key];
  if (typeof v === 'boolean') return v;
  return defaultIfUnset;
}

function slugForK8s(name: string): string {
  return name.trim().toLowerCase().replace(/[^a-z0-9]+/gu, '-').replace(/^-+|-+$/gu, '') || 'app';
}

function dockerBaseImage(tgtLang: string): string {
  const t = tgtLang.toLowerCase();
  if (t === 'java' || t === 'kotlin' || t === 'java_legacy') return 'eclipse-temurin:21-jre-alpine';
  if (t === 'python') return 'python:3.12-slim';
  if (t === 'node' || t === 'typescript' || t === 'javascript') return 'node:20-alpine';
  if (t === 'php' || t === 'php_legacy') return 'php:8.3-cli-alpine';
  if (t === 'go') return 'golang:1.22-alpine';
  if (t === 'rust') return 'rust:1-bookworm';
  return 'ubuntu:22.04';
}

type LangFamily = 'jvm' | 'node' | 'python' | 'dotnet' | 'go' | 'rust' | 'ruby' | 'php' | 'swift' | 'other';

/** Map a target language to its tooling family (drives linter/formatter choice). */
function family(tgtLang: string): LangFamily {
  const t = tgtLang.toLowerCase().replace(/[^a-z0-9_]/g, '');
  if (['java', 'kotlin', 'scala', 'groovy', 'java_legacy', 'clojure'].includes(t)) return 'jvm';
  if (['typescript', 'javascript', 'node'].includes(t)) return 'node';
  if (t === 'python') return 'python';
  if (['csharp', 'cs', 'fsharp', 'dotnet_legacy', 'vbnet'].includes(t)) return 'dotnet';
  if (t === 'go') return 'go';
  if (t === 'rust') return 'rust';
  if (t === 'ruby') return 'ruby';
  if (t === 'php' || t === 'php_legacy') return 'php';
  if (t === 'swift') return 'swift';
  return 'other';
}

function editorconfig(glob: string, style: 'space' | 'tab', size: number, maxLen: number): string {
  return `root = true

[${glob}]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = ${style}
indent_size = ${size}
max_line_length = ${maxLen}
`;
}

type ConfigArtifact = { path: string; lang: string; tool: string; code: string };

/** Idiomatic linter config for the target language family, or null when none applies. */
function linter(fam: LangFamily): ConfigArtifact | null {
  if (fam === 'node') return { path: '.eslintrc.json', lang: 'json', tool: 'ESLint', code: `{
  "root": true,
  "extends": ["eslint:recommended"],
  "ignorePatterns": ["dist/**", "build/**", "node_modules/**", ".next/**"]
}` };
  if (fam === 'jvm') return { path: 'checkstyle.xml', lang: 'xml', tool: 'Checkstyle', code: `<?xml version="1.0"?>
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
  <property name="charset" value="UTF-8"/>
  <module name="TreeWalker">
    <module name="UnusedImports"/>
    <module name="RedundantImport"/>
    <module name="NeedBraces"/>
    <module name="EmptyBlock"/>
    <module name="LeftCurly"/>
    <module name="RightCurly"/>
  </module>
</module>
` };
  if (fam === 'python') return { path: 'ruff.toml', lang: 'toml', tool: 'Ruff', code: `line-length = 100
target-version = "py312"

[lint]
select = ["E", "F", "I", "UP", "B"]
ignore = []
` };
  if (fam === 'dotnet') return { path: 'Directory.Build.props', lang: 'xml', tool: '.NET analyzers', code: `<Project>
  <PropertyGroup>
    <EnableNETAnalyzers>true</EnableNETAnalyzers>
    <AnalysisLevel>latest</AnalysisLevel>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
  </PropertyGroup>
</Project>
` };
  if (fam === 'go') return { path: '.golangci.yml', lang: 'yaml', tool: 'golangci-lint', code: `run:
  timeout: 5m
linters:
  enable:
    - govet
    - staticcheck
    - errcheck
    - ineffassign
    - unused
` };
  if (fam === 'ruby') return { path: '.rubocop.yml', lang: 'yaml', tool: 'RuboCop', code: `AllCops:
  NewCops: enable
  TargetRubyVersion: 3.2
Style/Documentation:
  Enabled: false
Layout/LineLength:
  Max: 120
` };
  if (fam === 'php') return { path: 'phpcs.xml', lang: 'xml', tool: 'PHP_CodeSniffer', code: `<?xml version="1.0"?>
<ruleset name="Scriba">
  <description>PSR-12 coding standard</description>
  <rule ref="PSR12"/>
  <file>src</file>
</ruleset>
` };
  if (fam === 'rust') return { path: 'clippy.toml', lang: 'toml', tool: 'Clippy', code: `# Run: cargo clippy --all-targets -- -D warnings
cognitive-complexity-threshold = 30
too-many-arguments-threshold = 8
` };
  if (fam === 'swift') return { path: '.swiftlint.yml', lang: 'yaml', tool: 'SwiftLint', code: `disabled_rules:
  - trailing_whitespace
line_length: 120
included:
  - Sources
` };
  return { path: '.editorconfig', lang: 'ini', tool: 'EditorConfig', code: editorconfig('*', 'space', 2, 120) };
}

/** Idiomatic formatter config for the target language family, or null when covered elsewhere. */
function formatter(fam: LangFamily): ConfigArtifact | null {
  if (fam === 'node') return { path: '.prettierrc', lang: 'json', tool: 'Prettier', code: `{
  "semi": true,
  "singleQuote": true,
  "printWidth": 100,
  "trailingComma": "es5"
}` };
  if (fam === 'rust') return { path: 'rustfmt.toml', lang: 'toml', tool: 'rustfmt', code: `edition = "2021"
max_width = 100
tab_spaces = 4
` };
  if (fam === 'swift') return { path: '.swift-format', lang: 'json', tool: 'swift-format', code: `{
  "version": 1,
  "lineLength": 100,
  "indentation": { "spaces": 4 }
}` };
  if (fam === 'jvm') return { path: '.editorconfig', lang: 'ini', tool: 'EditorConfig', code: editorconfig('*.{java,kt,kts,groovy,scala}', 'space', 4, 120) };
  if (fam === 'python') return { path: '.editorconfig', lang: 'ini', tool: 'EditorConfig', code: editorconfig('*.py', 'space', 4, 100) };
  if (fam === 'dotnet') return { path: '.editorconfig', lang: 'ini', tool: 'EditorConfig (dotnet format)', code: editorconfig('*.{cs,fs,vb}', 'space', 4, 120) };
  if (fam === 'go') return { path: '.editorconfig', lang: 'ini', tool: 'gofmt / EditorConfig', code: editorconfig('*.go', 'tab', 4, 120) };
  if (fam === 'ruby') return { path: '.editorconfig', lang: 'ini', tool: 'EditorConfig', code: editorconfig('*.rb', 'space', 2, 120) };
  if (fam === 'php') return { path: '.editorconfig', lang: 'ini', tool: 'EditorConfig', code: editorconfig('*.php', 'space', 4, 120) };
  return null;
}

/**
 * Minimal import-based fallback manifest generator used only when the
 * scriba-engine is offline. Reads actual imports from the converted files
 * instead of assuming a framework — does NOT hardcode dependency lists.
 * When the engine is online, the AI scaffold call replaces this entirely.
 */
function buildFallbackManifestFiles(args: {
  tgtLang: string;
  srcLang: string;
  projectName: string;
  engineFiles: Array<Record<string, unknown>>;
}): ToolingArtifactFile[] {
  const { tgtLang, srcLang, projectName, engineFiles } = args;
  const t = tgtLang.toLowerCase().replace(/[^a-z0-9_]/g, '');
  const slug = slugForK8s(projectName);
  const safeName = (projectName || 'project').replace(/"/g, "'");
  const out: ToolingArtifactFile[] = [];

  function push(id: string, path: string, lang: string, description: string, code: string) {
    const name = path.split('/').pop() ?? path;
    out.push({ id, name, path, lang, lines: Math.max(1, code.split('\n').length), status: 'info', description, code });
  }

  if (t === 'typescript' || t === 'javascript' || t === 'node') {
    // Extract third-party imports from the converted files
    const importRe = /\bimport\s+(?:[^"']+?from\s+)?["']([^"'./][^"']*?)["']/g;
    const pkgs = new Set<string>();
    const nodeBuiltins = new Set(['fs', 'path', 'os', 'crypto', 'util', 'stream', 'buffer', 'http', 'https', 'url', 'events', 'child_process', 'net']);
    for (const ef of engineFiles) {
      const content = typeof ef.content === 'string' ? ef.content : '';
      for (const m of content.matchAll(importRe)) {
        const spec = m[1] ?? '';
        if (!spec || spec.startsWith('node:')) continue;
        const base = spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0] ?? '';
        if (base && !nodeBuiltins.has(base)) pkgs.add(base);
      }
    }
    const depsEntries = [...pkgs].sort().map((p) => `    "${p}": "latest"`).join(',\n');
    const depsBlock = depsEntries ? `\n${depsEntries}\n  ` : '';
    const pkg = `{
  "name": "${slug}",
  "version": "1.0.0",
  "description": "Migrated from ${srcLang} by Scriba",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "test": "node --test",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {${depsBlock}},
  "devDependencies": {
    "typescript": "^5.4.0",
    "@types/node": "^20.0.0"
  }
}`;
    push('manifest-pkg', 'package.json', 'json', `npm manifest — deps from actual imports (migrated from ${srcLang})`, pkg);
    push('manifest-tsconfig', 'tsconfig.json', 'json', 'TypeScript compiler config', `{
  "compilerOptions": {
    "target": "es2022",
    "module": "esnext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "dist"
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules", "dist"]
}`);
    push('manifest-env', '.env.example', 'plaintext', '.env template', `NODE_ENV=development\nPORT=3000\n`);
    return out;
  }

  // All other languages: emit a minimal SETUP.md pointing the user to run the engine
  const setup = `# ${safeName} — Project Setup

Migrated from **${srcLang}** to **${tgtLang}** by Scriba.

> **Note:** Project manifest files (${t === 'java' ? 'pom.xml' : t === 'python' ? 'pyproject.toml' : t === 'go' ? 'go.mod' : t === 'rust' ? 'Cargo.toml' : 'build files'}) are generated by the Scriba Engine AI when the engine is running. Connect the engine and re-run the migration to get fully populated dependency manifests.

## Manual setup

1. Install your ${tgtLang} runtime and package manager.
2. Review the converted source files for their import/require statements.
3. Create the appropriate manifest (e.g. \`${t === 'java' ? 'pom.xml' : t === 'python' ? 'pyproject.toml' : t === 'go' ? 'go.mod' : t === 'kotlin' ? 'build.gradle.kts' : 'project manifest'}\`) and add the detected dependencies.
4. Configure environment variables from the original project.
`;
  push('manifest-setup', 'SETUP.md', 'markdown', `Setup instructions (migrated from ${srcLang})`, setup);
  return out;
}

/** Synthetic config / infra files driven by create-project wizard toggles. */
function buildToolingArtifacts(args: {
  cfg: Record<string, unknown>;
  tgtLang: string;
  srcLang: string;
  projectName: string;
}): ToolingArtifactFile[] {
  const { cfg, tgtLang, srcLang, projectName } = args;
  const safeName = (projectName || 'project').replace(/"/gu, "'");
  const slug = slugForK8s(safeName);
  const out: ToolingArtifactFile[] = [];

  const wantLinter = wizardEnabled(cfg, 'addLinter', true);
  const wantFormatter = wizardEnabled(cfg, 'addFormatter', true);
  const wantTypeScript = wizardEnabled(cfg, 'addTypeScript', false);
  const wantDocker = wizardEnabled(cfg, 'addDocker', true);
  const wantK8s = wizardEnabled(cfg, 'addKubernetes', false);
  const wantCI = wizardEnabled(cfg, 'addCI', true);
  const wantOpenAPI = wizardEnabled(cfg, 'enableOpenAPISpec', true);

  const fam = family(tgtLang);
  const add = (id: string, a: ConfigArtifact, description: string) => {
    if (out.some(f => f.path === a.path)) return;
    out.push({ id, name: a.path.split('/').pop() ?? a.path, path: a.path, lang: a.lang, lines: Math.max(1, a.code.split('\n').length), status: 'info', description, code: a.code });
  };

  if (wantLinter) {
    const a = linter(fam);
    if (a) add('art-lint', a, `Lint rules — ${a.tool} (wizard: linting enabled)`);
  }

  if (wantFormatter) {
    const a = formatter(fam);
    if (a) add('art-fmt', a, `Formatter config — ${a.tool} (wizard: formatter enabled)`);
  }

  if (wantTypeScript) {
    const code = `{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true
  },
  "include": ["src/**/*", "lib/**/*"]
}`;
    out.push({
      id: 'art-ts',
      name: 'tsconfig.scriba.json',
      path: 'tsconfig.scriba.json',
      lang: 'json',
      lines: Math.max(1, code.split('\n').length),
      status: 'info',
      description: 'TypeScript config (wizard: TypeScript enabled)',
      code,
    });
  }

  if (wantDocker) {
    const base = dockerBaseImage(tgtLang);
    const code = `# Scriba starter image — ${srcLang} → ${tgtLang} (${safeName})
FROM ${base}
WORKDIR /app
COPY . .
# Add build + runtime CMD for your stack
CMD ["echo","Configure CMD for ${tgtLang}"]
`;
    out.push({
      id: 'art-docker',
      name: 'Dockerfile',
      path: 'Dockerfile',
      lang: 'docker',
      lines: Math.max(1, code.split('\n').length),
      status: 'info',
      description: 'Container entry (wizard: Docker enabled)',
      code,
    });
  }

  if (wantK8s) {
    const code = `apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${slug}-migrated
spec:
  replicas: 1
  selector:
    matchLabels: { app: scriba-migrated }
  template:
    metadata:
      labels: { app: scriba-migrated }
    spec:
      containers:
        - name: app
          image: your-registry/${slug}:latest
          ports:
            - containerPort: 8080
`;
    out.push({
      id: 'art-k8s',
      name: 'deployment.yaml',
      path: 'k8s/deployment.yaml',
      lang: 'yaml',
      lines: Math.max(1, code.split('\n').length),
      status: 'info',
      description: 'Kubernetes stub (wizard: Kubernetes enabled)',
      code,
    });
  }

  if (wantOpenAPI) {
    const code = `openapi: 3.0.3
info:
  title: ${safeName} API
  version: 1.0.0
  description: Placeholder after migration ${srcLang} → ${tgtLang}
paths: {}
`;
    out.push({
      id: 'art-openapi',
      name: 'openapi.yaml',
      path: 'openapi.yaml',
      lang: 'yaml',
      lines: Math.max(1, code.split('\n').length),
      status: 'info',
      description: 'OpenAPI stub (wizard: OpenAPI spec enabled)',
      code,
    });
  }

  if (wantCI) {
    const yaml = buildScribaDeployWorkflow({
      projectName: safeName,
      sourceLanguage: srcLang,
      targetLanguage: tgtLang,
      config: cfg,
      includeOptionalDocker: wantDocker,
    });
    out.push({
      id: 'art-ci',
      name: 'scriba-deploy.yml',
      path: '.github/workflows/scriba-deploy.yml',
      lang: 'yaml',
      lines: Math.max(1, yaml.split('\n').length),
      status: 'info',
      description: 'GitHub Actions workflow (wizard: CI/CD enabled)',
      code: yaml,
    });
  }

  return out;
}

interface Props {
  projectId: string;
  onNavigate: (section: string, projectId?: string) => void;
  project?: Project | null;
  onProjectUpdate?: () => void;
}

export default function MigrationFlow({ projectId, onNavigate, project, onProjectUpdate }: Props) {
  const { user: sessionUser } = useSession();
  const isAdmin = sessionUser?.role === 'admin';
  const [currentPhase, setCurrentPhase] = useState<MigrationPhase>('pre-analysis');
  const [steps, setSteps] = useState<MigrationStep[]>([]);
  const [progress, setProgress] = useState(0);
  const [showDetails, setShowDetails] = useState(true);
  const [showLogs, setShowLogs] = useState(true);
  const [logFilter, setLogFilter] = useState<'all' | 'info' | 'success' | 'warning' | 'error'>('all');
  const [logSearch, setLogSearch] = useState('');
  const [conversionResult, setConversionResult] = useState<Record<string, unknown> | null>(null);
  const [engineOnline, setEngineOnline] = useState<boolean | null>(null);
  const [engineAlive, setEngineAlive] = useState<boolean | null>(null);
  const [cancelConfirm, setCancelConfirm] = useState(false);
  const [activeConversionId, setActiveConversionId] = useState<string | null>(null);
  const [activeRunId, setActiveRunId] = useState<string | null>(null);
  const [showReviewBoard, setShowReviewBoard] = useState(false);
  const [liveUsage, setLiveUsage] = useState<RunUsageSnapshot | null>(null);
  const [hilApprovedFiles, setHilApprovedFiles] = useState<string[]>([]);
  const [hilPendingPaths, setHilPendingPaths] = useState<string[]>([]);
  const [hilDiffFile, setHilDiffFile] = useState<string | null>(null);
  const [hilDiff, setHilDiff] = useState<{ source: string; target: string } | null>(null);
  const [logs, setLogs] = useState<
    { id: string; time: string; level: string; message: string; category?: EngineLogCategory }[]
  >([]);
  const [costApprovalDone, setCostApprovalDone] = useState(false);
  const [bundleUploadStatus, setBundleUploadStatus] = useState<string | null>(null);
  const [quotaError, setQuotaError] = useState<{
    reason?: 'rate-limit' | 'concurrency-cap';
    observed?: { runs_last_hour?: number; in_flight?: number };
    limits?: { runs_per_hour?: number; max_concurrent?: number };
    retry_after_s?: number;
  } | null>(null);
  const [quotaCountdown, setQuotaCountdown] = useState(0);
  const [budgetExceeded, setBudgetExceeded] = useState<{ message?: string } | null>(null);
  const [runLost, setRunLost] = useState(false);
  const [liveCostUsd, setLiveCostUsd] = useState(0);
  const [queuePosition, setQueuePosition] = useState<{ depth: number; position: number } | null>(null);
  const [detectedProfile, setDetectedProfile] = useState<string | null>(null);
  const [decisionJournalOpen, setDecisionJournalOpen] = useState(false);
  const [decisionJournalContent, setDecisionJournalContent] = useState<string | null>(null);
  const [decisionJournalLoading, setDecisionJournalLoading] = useState(false);

  // Sandbox run state — auto-triggered after successful migration
  const [sandboxPhase, setSandboxPhase] = useState<'idle' | 'running' | 'success' | 'failed'>('idle');
  const [sandboxLogs, setSandboxLogs] = useState<Array<{ id: string; line: string; stream: 'stdout' | 'stderr'; level: string; elapsed?: string }>>([]);
  const [sandboxAttempt, setSandboxAttempt] = useState(0);
  const [sandboxDuration, setSandboxDuration] = useState<number | null>(null);
  const [sandboxExitCode, setSandboxExitCode] = useState<number | null>(null);
  const [sandboxElapsed, setSandboxElapsed] = useState(0);
  const sandboxLogsRef = useRef<Array<{ id: string; line: string; stream: 'stdout' | 'stderr'; level: string; elapsed?: string }>>([]);
  const sandboxIdRef = useRef(0);
  const sandboxRunnerRef = useRef<((attempt: number) => Promise<void>) | null>(null);
  const sandboxLogEndRef = useRef<HTMLDivElement | null>(null);
  const sandboxStartMsRef = useRef<number | null>(null);
  const sandboxElapsedTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const progressTickerRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const progressRef = useRef(0);

  const esRef = useRef<EventSource | null>(null);
  const lastSseEventIdRef = useRef<string | undefined>(undefined);
  const sseReconnectAttemptRef = useRef(0);
  const logIdRef = useRef(0);
  const hasReconnectedRef = useRef(false);
  const hasLoggedInitialFailedRef = useRef(false);
  const migrationStartTimeRef = useRef<number | null>(null);
  /** Smoothed ETA (seconds) to avoid jumpy/over-stated estimates. */
  const etaRef = useRef<number | null>(null);
  /** SSE line classified as structured failure — used when `done` lacks metadata.errors */
  const lastStructuralEngineFailureRef = useRef<string | undefined>(undefined);

  const addLog = (level: string, message: string, extra?: { category?: EngineLogCategory }) => {
    const safeMsg = sanitizeEngineLogForDisplay(String(message ?? ''));
    if (isStructuralFailureEngineLogLine(safeMsg)) {
      lastStructuralEngineFailureRef.current = safeMsg;
    }
    const now = new Date();
    const time = `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}.${String(now.getMilliseconds()).padStart(3,'0')}`;
    setLogs(prev => [
      ...prev,
      {
        id: String(++logIdRef.current),
        time,
        level,
        message: safeMsg,
        ...(extra?.category && extra.category !== 'generic' && extra.category !== 'info' ? { category: extra.category } : {}),
      },
    ]);
    if (projectId) {
      api.saveLog(projectId, time, level, safeMsg).catch(() => {});
    }
  };

  const calculateETA = (): string => {
    if (progress >= 100) return '—';
    if (!migrationStartTimeRef.current || progress <= 0) return 'Estimating…';
    const elapsedSeconds = (Date.now() - migrationStartTimeRef.current) / 1000;
    // Too little signal early on → a linear extrapolation is wildly inaccurate
    // (e.g. 3% after 40s would read ~20m). Show "Estimating…" until it stabilises.
    if (elapsedSeconds < 30 || progress < 10) return 'Estimating…';

    const rawRemaining = elapsedSeconds * (100 - progress) / progress;
    if (!isFinite(rawRemaining) || rawRemaining < 0) return '—';
    // Exponential smoothing so the estimate doesn't jump around between progress ticks.
    etaRef.current = etaRef.current == null ? rawRemaining : etaRef.current * 0.6 + rawRemaining * 0.4;
    const remainingSeconds = etaRef.current;

    if (remainingSeconds < 60) {
      return `~${Math.round(remainingSeconds)}s`;
    } else if (remainingSeconds < 3600) {
      const minutes = Math.floor(remainingSeconds / 60);
      const seconds = Math.round(remainingSeconds % 60);
      return seconds > 0 ? `~${minutes}m ${seconds}s` : `~${minutes}m`;
    } else {
      const hours = Math.floor(remainingSeconds / 3600);
      const minutes = Math.floor((remainingSeconds % 3600) / 60);
      return minutes > 0 ? `~${hours}h ${minutes}m` : `~${hours}h`;
    }
  };

  const filteredLogs = logs.filter(log => {
    if (logFilter !== 'all' && log.level !== logFilter) return false;
    if (logSearch && !log.message.toLowerCase().includes(logSearch.toLowerCase())) return false;
    return true;
  });

  // Check engine health on mount & hydrate completed state
  useEffect(() => {
    api.engine.health().then(() => setEngineOnline(true)).catch(() => setEngineOnline(false));
    initializeSteps(getEnabledPluginSet(parseProjectConfig(project)));

    // Load persisted logs from DB
    if (projectId) {
      api.getLogs(projectId).then(data => {
        if (data.logs.length > 0) {
          const loaded = data.logs.map(l => ({
            id: String(l.id),
            time: l.time,
            level: l.level,
            message: l.message,
          }));
          logIdRef.current = data.logs[data.logs.length - 1].id;
          setLogs(loaded);
        }
      }).catch(() => {});
    }

    // If project is already completed, restore finished state from config
    const status = (project as any)?.status;
    const saved = (project as any)?.config?.conversionResult;
    if (status === 'completed' && saved) {
      setCurrentPhase('completed');
      setProgress(100);
      setConversionResult(saved);
      setSteps(prev => prev.map(s => ({ ...s, status: 'completed' as const })));
      const savedCompletedRunId = (project as any)?.config?.completedRunId as string | undefined;
      if (savedCompletedRunId) setActiveRunId(savedCompletedRunId);
    } else if (status === 'failed') {
      setCurrentPhase('failed');
      setProgress(0);
      const failedSaved = parseProjectConfig(project).conversionResult as Record<string, unknown> | undefined;
      if (failedSaved && typeof failedSaved === 'object') setConversionResult(failedSaved);
      if (!hasLoggedInitialFailedRef.current) {
        hasLoggedInitialFailedRef.current = true;
        addLog('error', 'Previous migration failed. You can retry.');
      }
    } else if (status === 'converting') {
      const savedRunId = (project as any)?.config?.activeRunId as string | undefined;
      const savedConvId = (project as any)?.config?.activeConversionId as string | undefined;
      // Stuck-converting: engine finished but the status update was lost — treat as completed
      if (!savedConvId && !savedRunId && (project as any)?.config?.conversionResult) {
        const stuckResult = (project as any)?.config?.conversionResult;
        setCurrentPhase('completed');
        setProgress(100);
        setConversionResult(stuckResult);
        setSteps(prev => prev.map(s => ({ ...s, status: 'completed' as const })));
      } else if (savedConvId) {
        const rawSrc = (project as any)?.source_language || (project as any)?.sourceLanguage;
        const rawTgt = (project as any)?.target_language || (project as any)?.targetLanguage;
        const rSrc = typeof rawSrc === 'string' && rawSrc.trim() ? normalizeProjectLang(rawSrc) : '';
        const rTgt = typeof rawTgt === 'string' && rawTgt.trim() ? normalizeProjectLang(rawTgt) : '';
        if (rSrc && rTgt) {
          setCurrentPhase('migration');
          if (savedRunId) setActiveRunId(savedRunId);
          if (!hasReconnectedRef.current) {
            addLog('info', 'Reconnecting to migration in progress...');
            hasReconnectedRef.current = true;
          }
          const rCfg = parseProjectConfig(project);
          subscribeToConversion(savedConvId, rSrc, rTgt, rCfg, savedRunId).catch(() => {});
        }
      }
    }

    return () => { esRef.current?.close(); };
  }, []);

  // Poll run usage while a platform run is in flight
  useEffect(() => {
    if (!activeRunId) return;
    if (currentPhase === 'completed' || currentPhase === 'failed' || currentPhase === 'pre-analysis') return;

    let cancelled = false;
    const poll = async () => {
      try {
        const usage = await api.engine.getRunUsage(activeRunId);
        if (!cancelled) setLiveUsage(usage as RunUsageSnapshot);
      } catch {
        /* non-blocking */
      }
    };
    poll();
    const interval = setInterval(poll, 8000);
    return () => {
      cancelled = true;
      clearInterval(interval);
    };
  }, [activeRunId, currentPhase]);

  // Restore completed state when project prop arrives/updates (handles late-loading currentProject)
  useEffect(() => {
    const status = (project as any)?.status;
    const saved = (project as any)?.config?.conversionResult;
    if (status === 'completed' && saved && currentPhase === 'pre-analysis') {
      setCurrentPhase('completed');
      setProgress(100);
      setConversionResult(saved);
      setSteps(prev => prev.map(s => ({ ...s, status: 'completed' as const })));
    }
  }, [(project as any)?.status, (project as any)?.id]);

  // Sync costApprovalDone from project config; admins skip the gate entirely.
  // Depend on costApprovedAt (direct DB column) so a remount after approval restores the flag
  // without relying solely on the JSONB config field being present in the cached project prop.
  useEffect(() => {
    if (isAdmin) { setCostApprovalDone(true); return; }
    if (project) {
      const cfg = parseProjectConfig(project);
      if (cfg.costApproved || (project as any)?.costApprovedAt) setCostApprovalDone(true);
    }
  }, [(project as any)?.id, (project as any)?.costApprovedAt, isAdmin]);

  // Count down the quota retry-after timer
  useEffect(() => {
    if (!quotaError?.retry_after_s) return;
    setQuotaCountdown(quotaError.retry_after_s);
    const tick = setInterval(() => {
      setQuotaCountdown(prev => {
        if (prev <= 1) { clearInterval(tick); return 0; }
        return prev - 1;
      });
    }, 1000);
    return () => clearInterval(tick);
  }, [quotaError]);

  const handleCostApproval = async () => {
    try {
      await api.approveConversionCost(projectId);
    } catch {
      return;
    }
    setCostApprovalDone(true);
    onProjectUpdate?.();
  };

  const handleBundleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const fileList = e.target.files;
    if (!fileList?.length) return;
    setBundleUploadStatus('Uploading source bundle to engine...');
    try {
      const files = Array.from(fileList);
      // Read file content for the estimate call (parallel with upload)
      const readFiles = async () => {
        const results: Array<{ path: string; content: string }> = [];
        await Promise.all(
          files.map(async (file) => {
            if (file.size > 200_000) return; // skip large/binary files
            try {
              const content = await file.text();
              results.push({ path: file.webkitRelativePath || file.name, content });
            } catch {
              // skip unreadable files
            }
          }),
        );
        return results;
      };

      const [prep, fileContents] = await Promise.all([
        api.engine.uploadBundle(files),
        readFiles(),
      ]);

      // Call /estimate + /preflight in parallel — both failures are non-fatal
      let engineEstimate: { p95CostUsd: number; expectedCostUsd: number; effectiveLines: number; byFile?: Array<{ path: string; effectiveLines: number; predictedCostUsd: number }> } | undefined;
      let preflightSummary: { byLanguage: Record<string, number>; dialects: Record<string, number>; execSqlBlocksTotal: number; execSqlCallsTotal: number; totalLines: number; nonPortableTotal: number; graph?: unknown; datasets?: string[]; datasetFlow?: { externalInputs: string[]; terminalOutputs: string[] }; mainframeArtefacts?: { bms: number; dbd: number; psb: number; csd: number; ddl: number; map: number } } | undefined;
      if (fileContents.length > 0) {
        const maxIter =
          typeof (project?.config as Record<string, unknown> | null | undefined)?.maxIterations === 'number'
            ? (project?.config as Record<string, unknown>).maxIterations as number
            : 5;
        const [estResult, pfResult] = await Promise.allSettled([
          api.engine.estimate(fileContents, { maxIter, breakdown: true }),
          api.engine.preflight(fileContents),
        ]);
        if (estResult.status === 'fulfilled') {
          const est = estResult.value;
          engineEstimate = {
            p95CostUsd: est.p95.costUsd,
            expectedCostUsd: est.expected.costUsd,
            effectiveLines: est.source.effectiveLines,
            ...(est.byFile ? { byFile: est.byFile } : {}),
            ...(est.assumptions ? {
              calibrationSource: (est.assumptions as Record<string, unknown>).source as 'empirical' | 'interpolated' | 'synthetic' | undefined,
              calibrationCorpus: (est.assumptions as Record<string, unknown>).corpus as string | undefined,
            } : {}),
          };
        }
        if (pfResult.status === 'fulfilled') {
          const pf = pfResult.value;
          preflightSummary = {
            byLanguage: pf.byLanguage,
            dialects: pf.dialects,
            execSqlBlocksTotal: pf.execSqlBlocksTotal,
            execSqlCallsTotal: pf.execSqlCallsTotal,
            totalLines: pf.totalLines,
            nonPortableTotal: pf.nonPortableTotal,
            ...(pf.graph ? { graph: pf.graph } : {}),
            ...(pf.datasets && pf.datasets.length > 0 ? { datasets: pf.datasets } : {}),
            ...(pf.datasetFlow ? { datasetFlow: pf.datasetFlow } : {}),
            ...(pf.mainframeArtefacts ? { mainframeArtefacts: pf.mainframeArtefacts } : {}),
          };
        }
      }

      await api.updateProject(projectId, {
        config: {
          uploadId: prep.uploadId,
          uploadMeta: {
            fileCount: prep.fileCount,
            totalBytes: prep.totalBytes,
            preparedAt: new Date().toISOString(),
            source: 'browser',
          },
          ...(engineEstimate ? { engineEstimate } : {}),
          ...(preflightSummary ? { preflightSummary } : {}),
        },
      });
      setBundleUploadStatus(`Ready — ${prep.fileCount} files uploaded (${Math.round(prep.totalBytes / 1024)} KB)`);
      onProjectUpdate?.();
    } catch (err) {
      setBundleUploadStatus(`Upload failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
    }
  };

  const initializeSteps = (enabledPluginSet: Set<string> | null = null, sourceLang?: string) => {
    const pluginStatus = (id: string): MigrationStep['status'] =>
      PLUGIN_IDS.has(id) && !isPluginEnabled(id, enabledPluginSet) ? 'skipped' : 'pending';

    const isCobol = (sourceLang ?? normalizeProjectLang((project as any)?.source_language || (project as any)?.sourceLanguage || '')) === 'cobol';

    setSteps([
      {
        id: 'scan-repo',
        name: 'Repository Scan',
        description: 'Scanning repository structure and files',
        status: 'pending',
      },
      {
        id: 'analyze-dependencies',
        name: 'Dependency Analysis',
        description: 'Mapping file dependencies and call graphs',
        status: 'pending',
      },
      {
        id: 'assess-complexity',
        name: 'Complexity Assessment',
        description: 'Evaluating code complexity and risk factors',
        status: 'pending',
      },
      {
        id: 'validate-config',
        name: 'Configuration Validation',
        description: 'Validating migration configuration',
        status: 'pending',
      },
      {
        id: 'pre-flight-checks',
        name: 'Pre-flight Checks',
        description: 'Running environment and resource checks',
        status: 'pending',
      },
      ...(isCobol ? [{
        id: 'cobol-dependency-map',
        name: 'COBOL Dependency Mapping',
        description: 'Resolving copybooks, CALL targets, and inter-program dependencies',
        status: 'pending' as MigrationStep['status'],
      }] : []),
      {
        id: 'parsing',
        name: 'Source Parsing',
        description: 'Lexical analysis and AST construction',
        status: 'pending',
      },
      {
        id: 'semantic-analysis',
        name: 'Semantic Analysis',
        description: 'Type checking and control flow analysis',
        status: 'pending',
      },
      {
        id: 'neural-translation',
        name: 'Neural Translation',
        description: 'AI-powered code generation',
        status: 'pending',
      },
      {
        id: 'syntax-validation',
        name: 'Syntax Validation',
        description: 'Verifying generated code syntax',
        status: pluginStatus('syntax-validation'),
      },
      {
        id: 'error-repair',
        name: 'Auto-Repair',
        description: 'AI-powered fix of compile errors found in generated code',
        status: 'pending',
      },
      {
        id: 'functional-validation',
        name: 'Functional Validation',
        description: 'Running test suite and parity checks',
        status: pluginStatus('functional-validation'),
      },
      {
        id: 'performance-analysis',
        name: 'Performance Analysis',
        description: 'Analyzing performance characteristics',
        status: pluginStatus('performance-analysis'),
      },
      {
        id: 'security-scan',
        name: 'Security Scan',
        description: 'Running security vulnerability analysis',
        status: pluginStatus('security-scan'),
      },
      {
        id: 'documentation',
        name: 'Documentation Generation',
        description: 'Generating API docs and code documentation',
        status: pluginStatus('documentation'),
      },
      {
        id: 'final-verification',
        name: 'Final Verification',
        description: 'Comprehensive quality gate verification',
        status: 'pending',
      },
      {
        id: 'test-generation',
        name: 'Test Generation',
        description: 'Generating unit and integration tests',
        status: pluginStatus('test-generation'),
      },
      {
        id: 'test-execution',
        name: 'Test Execution',
        description: 'Running generated tests against the output bundle',
        status: 'pending',
      },
    ]);
  };

  const subscribeToConversion = async (
    conversionId: string,
    srcLang: string,
    tgtLang: string,
    cfg: ReturnType<typeof parseProjectConfig>,
    runId?: string | null,
  ): Promise<void> => {
    setActiveConversionId(conversionId);
    if (runId) setActiveRunId(runId);
    const es = runId ? api.engine.streamRun(runId) : api.engine.streamConversion(conversionId);
    esRef.current = es;
    es.onopen = () => {
      setEngineAlive(true);
    };

    const phaseMap: Record<string, MigrationPhase> = {
      analysis: 'validation',
      migration: 'migration',
      verification: 'verification',
    };

    await new Promise<void>((resolve) => {
      /** After a valid `done` payload, ignore late EventSource errors (close/idle) while async work runs. */
      let streamSucceeded = false;

      /**
       * Poll /runs/:id/result with 409 retry loop (ML01 §3.6).
       * Called when SSE fails permanently — tries to recover the result before marking failed.
       * On 409: waits Retry-After seconds (max 30s) and retries until 200/410/404 or 5-min deadline.
       * On 200: transitions to completed with the recovered result.
       * On 410: marks run as lost.
       * All outcomes call resolve() exactly once.
       */
      const pollResult = async (pollRunId: string): Promise<void> => {
        const deadline = Date.now() + 5 * 60 * 1000;
        while (Date.now() < deadline) {
          let r: Awaited<ReturnType<typeof api.engine.getRunResult>>;
          try {
            r = await api.engine.getRunResult(pollRunId);
          } catch {
            break;
          }
          if (r._httpStatus === 200 && r.result) {
            streamSucceeded = true;
            hasReconnectedRef.current = false;
            setConversionResult(r.result);
            setProgress(100);
            setSteps(prev =>
              prev.map(s =>
                s.id === 'test-generation' || s.status === 'completed' || s.status === 'failed' || s.status === 'skipped'
                  ? s
                  : { ...s, status: 'completed' as const },
              ),
            );
            const resultStatus = String(r.result.status ?? 'success');
            enqueueMigrationNotify(
              projectId,
              resultStatus === 'failed' ? 'failure' : 'complete',
              `Status: ${resultStatus}; recovered via poll`,
            );
            await api.updateProject(projectId, {
              status: resultStatus === 'failed' || resultStatus === 'cancelled' ? 'failed' : 'completed',
              config: {
                conversionResult: r.result,
                activeRunId: null,
                activeConversionId: null,
                ...(resultStatus !== 'failed' && resultStatus !== 'cancelled' && pollRunId ? { completedRunId: pollRunId } : {}),
              },
            }).catch(() => {});
            setCurrentPhase(resultStatus === 'failed' || resultStatus === 'cancelled' ? 'failed' : 'completed');
            onProjectUpdate?.();
            addLog(
              resultStatus === 'failed' ? 'error' : 'success',
              resultStatus === 'failed'
                ? 'Migration failed (recovered via poll)'
                : 'Migration completed (recovered via poll)',
            );
            resolve();
            return;
          }
          if (r._httpStatus === 409) {
            const waitSecs = Math.min(r._retryAfterSecs ?? 10, 30);
            addLog('info', `Run in progress — checking again in ${waitSecs}s…`);
            await new Promise<void>(res => setTimeout(res, waitSecs * 1000));
            continue;
          }
          if (r._httpStatus === 410) {
            setRunLost(true);
            addLog('warning', 'Run result was not persisted — the engine may have crashed before flushing.');
            break;
          }
          // 404 or unexpected status — treat as failure
          break;
        }
        setCurrentPhase('failed');
        hasReconnectedRef.current = false;
        await api.updateProject(projectId, { status: 'failed', config: { activeRunId: null, activeConversionId: null } }).catch(() => {});
        onProjectUpdate?.();
        enqueueMigrationNotify(projectId, 'failure', 'Connection to engine lost');
        resolve();
      };

      es.addEventListener('phase', (e: MessageEvent) => {
        if (e.lastEventId) lastSseEventIdRef.current = e.lastEventId;
        const d = JSON.parse(e.data);
        const mapped = phaseMap[d.phase] ?? 'migration';
        setCurrentPhase(mapped);
        const rawPhase = typeof d.message === 'string' ? d.message : '';
        addLog('info', rawPhase, {
          category: categorizeEngineLogMessage(rawPhase),
        });
      });

      es.addEventListener('step', (e: MessageEvent) => {
        if (e.lastEventId) lastSseEventIdRef.current = e.lastEventId;
        const d = JSON.parse(e.data);
        // Never overwrite a step that was pre-marked skipped — the engine may not honour disabledPlugins.
        setSteps(prev => prev.map(s => s.id === d.stepId && s.status !== 'skipped' ? {
          ...s,
          status: d.status,
          description: d.message,
          metrics: d.metrics ? Object.fromEntries(Object.entries(d.metrics).map(([k, v]) => [k, Number(v)])) : s.metrics,
          details: d.details ?? s.details,
          duration: d.duration ?? s.duration,
        } : s));
        const rawStep = typeof d.message === 'string' ? d.message : '';
        addLog(d.status === 'completed' ? 'success' : d.status === 'failed' ? 'error' : 'info', `[${d.stepId}] ${rawStep}`, {
          category: categorizeEngineLogMessage(rawStep),
        });
      });

      es.addEventListener('progress', (e: MessageEvent) => {
        if (e.lastEventId) lastSseEventIdRef.current = e.lastEventId;
        const d = JSON.parse(e.data);
        progressRef.current = d.percent;
        setProgress(d.percent);
      });

      // §4.1 — heartbeat keeps connection alive through proxies; use as liveness indicator
      es.addEventListener('heartbeat', (e: MessageEvent) => {
        if (e.lastEventId) lastSseEventIdRef.current = e.lastEventId;
        setEngineAlive(true);
      });

      es.addEventListener('log', (e: MessageEvent) => {
        if (e.lastEventId) lastSseEventIdRef.current = e.lastEventId;
        const d = JSON.parse(e.data);
        const rawLog = typeof d.message === 'string' ? d.message : '';
        // FE-WIZ-2 §4.2 — capture resolved project profile from engine log
        const profileMatch = rawLog.match(/(?:Detected project profile|Using operator-pinned profile):\s*(\w+)/i);
        if (profileMatch?.[1]) setDetectedProfile(profileMatch[1]);
        addLog(d.level, rawLog, {
          category: categorizeEngineLogMessage(rawLog),
        });
      });

      // §3.d — running cost bar: accumulate per-stage usage events
      es.addEventListener('usage', (e: MessageEvent) => {
        if (e.lastEventId) lastSseEventIdRef.current = e.lastEventId;
        try {
          const d = JSON.parse(e.data) as { stage?: string; costUsd?: number; tokens?: number; calls?: number };
          if (typeof d.costUsd === 'number') {
            setLiveCostUsd(prev => prev + d.costUsd!);
          }
        } catch { /* ignore */ }
      });

      // §3.d — queue position display
      es.addEventListener('queueDepth', (e: MessageEvent) => {
        if (e.lastEventId) lastSseEventIdRef.current = e.lastEventId;
        try {
          const d = JSON.parse(e.data) as { depth?: number; position?: number };
          if (typeof d.depth === 'number') {
            setQueuePosition({ depth: d.depth, position: d.position ?? 1 });
          }
        } catch { /* ignore */ }
      });

      // ML01 §8 — HITL review-queue: engine pauses and lists files awaiting approval
      es.addEventListener('step', (e: MessageEvent) => {
        // Secondary handler just for review-queue — primary step handler above fires first
        try {
          const d = JSON.parse(e.data);
          if (d.stepId === 'review-queue' && Array.isArray(d.pendingPaths)) {
            setHilPendingPaths(d.pendingPaths as string[]);
          }
          if (d.stepId === 'test-execution' && d.metrics) {
            const passed = Number(d.metrics.passed ?? 0);
            const failed = Number(d.metrics.failed ?? 0);
            addLog(
              failed > 0 ? 'warning' : 'success',
              `Test execution: ${passed} passed, ${failed} failed`,
              { category: 'test-gen' },
            );
          }
        } catch {
          /* ignore parse errors — primary handler already logged */
        }
      });

      es.addEventListener('done', async (e: MessageEvent) => {
        let d: { result?: Record<string, unknown> };
        try {
          d = JSON.parse(e.data);
        } catch {
          addLog('error', 'Migration finished but the engine response could not be parsed.');
          setCurrentPhase('failed');
          hasReconnectedRef.current = false;
          api.updateProject(projectId, { status: 'failed', config: { activeRunId: null, activeConversionId: null } }).then(() => {
            onProjectUpdate?.();
          }).catch(() => {});
          enqueueMigrationNotify(projectId, 'failure', 'Engine response could not be parsed');
          es.onerror = null;
          es.close();
          resolve();
          return;
        }
        const result = d.result;
        if (!result || typeof result !== 'object') {
          addLog('error', 'Migration finished without a conversion result.');
          setCurrentPhase('failed');
          hasReconnectedRef.current = false;
          api.updateProject(projectId, { status: 'failed', config: { activeRunId: null, activeConversionId: null } }).then(() => {
            onProjectUpdate?.();
          }).catch(() => {});
          enqueueMigrationNotify(projectId, 'failure', 'No conversion result payload');
          es.onerror = null;
          es.close();
          resolve();
          return;
        }

        streamSucceeded = true;
        hasReconnectedRef.current = false;
        const resultStatus = String((result as any)?.status ?? 'success');
        // Hold at 90 — test generation still pending; will reach 100 in the finally block
        setProgress(90);

        const baseMr = (result as { metadata?: unknown }).metadata;
        const baseMeta =
          baseMr != null && typeof baseMr === 'object' && !Array.isArray(baseMr)
            ? { ...(baseMr as Record<string, unknown>) }
            : {};
        const initialErrs = Array.isArray(baseMeta.errors)
          ? (baseMeta.errors as unknown[]).filter((e): e is string => typeof e === 'string')
          : [];
        let patchedMeta = baseMeta;
        let metaInjectedForFailure = false;
        if (resultStatus === 'failed' && initialErrs.length === 0 && lastStructuralEngineFailureRef.current) {
          patchedMeta = {
            ...baseMeta,
            errors: [lastStructuralEngineFailureRef.current],
          };
          metaInjectedForFailure = true;
        }
        const resultWithMeta = (
          metaInjectedForFailure
            ? { ...(result as Record<string, unknown>), metadata: patchedMeta }
            : (result as Record<string, unknown>)
        ) as Record<string, unknown>;

        // Extract real token count from engine accounting snapshot (provider-agnostic totalTokens).
        // Prefer result.metadata.accounting; if absent or zero, fetch /runs/:id/usage as a fallback
        // so the migration step always charges actual tokens rather than the fixed estimation formula.
        const engineAccounting = baseMeta.accounting as { totals?: { totalTokens?: number } } | undefined;
        let engineActualTokens = engineAccounting?.totals?.totalTokens ?? 0;
        if (engineActualTokens === 0 && runId) {
          try {
            const usageData = await api.engine.getRunUsage(runId);
            const usageAcct = usageData.accounting as { totals?: { totalTokens?: number } } | null;
            engineActualTokens = usageAcct?.totals?.totalTokens ?? 0;
          } catch {
            // ignore — step route will fall back to estimation formula
          }
        }

        if (resultStatus !== 'failed' && resultStatus !== 'cancelled') {
          const tokenMeta = engineActualTokens > 0 ? { actualTokens: engineActualTokens } : undefined;
          api.recordStepProgress(projectId, 3, 'completed', 8, tokenMeta).catch(() => {});
        }

        setConversionResult(resultWithMeta);
        if (resultStatus !== 'failed' && resultStatus !== 'cancelled') {
          // Engine can emit `done` without a terminal `step` event; mirror hydrate so the checklist does not stay on "running".
          // Exclude test-generation — it runs after the engine finishes and manages its own status.
          setSteps(prev =>
            prev.map(s =>
              s.id === 'test-generation' || s.status === 'completed' || s.status === 'failed' || s.status === 'skipped'
                ? s
                : { ...s, status: 'completed' as const },
            ),
          );
        }

        const engineFiles = (result as any)?.files ?? [];
        const meta = patchedMeta;
        const analysisReport: AnalysisReportShape | undefined = (result as any)?.analysisReport ?? undefined;
        const scribaReport = (result as any)?.scribaReport ?? null;
        const warnFromMeta = Array.isArray(meta.warnings)
          ? (meta.warnings as unknown[]).filter((w): w is string => typeof w === 'string')
          : [];
        const errFromMeta = Array.isArray(meta.errors)
          ? (meta.errors as unknown[]).filter((e): e is string => typeof e === 'string')
          : [];
        const hasWarnings = resultStatus === 'partial' || warnFromMeta.length > 0 || (errFromMeta.length > 0 && resultStatus !== 'failed');

        if (resultStatus === 'failed') {
          enqueueMigrationNotify(projectId, 'failure', errFromMeta[0] ?? 'Conversion failed');
        } else {
          if (hasWarnings) {
            enqueueMigrationNotify(
              projectId,
              'warning',
              warnFromMeta.slice(0, 5).join('; ') || errFromMeta.slice(0, 3).join('; ') || 'Completed with warnings',
            );
          }
          const notifyQ = (result as { quality?: { qualityIndex?: number } | null }).quality;
          const qLine =
            notifyQ != null &&
            typeof notifyQ.qualityIndex === 'number' &&
            Number.isFinite(notifyQ.qualityIndex)
              ? `; quality index ${notifyQ.qualityIndex}`
              : '';
          enqueueMigrationNotify(
            projectId,
            'complete',
            `Status: ${resultStatus || 'success'}; parity score ${(meta as { accuracy?: number }).accuracy ?? '?'}%${qLine}`,
          );
        }

        // Build analysisResults.translation from engine output
        // Fetch source code for each file so CodeComparison can show side-by-side
        const translationFiles = await Promise.all(
          engineFiles.map(async (ef: any, idx: number) => {
            let sourceCode = typeof ef.sourceContent === 'string' && ef.sourceContent.length > 0 ? ef.sourceContent : '';
            if (!sourceCode) {
              try {
                if (ef.sourcePath) {
                  const srcRes = await fetch(`/api/source-file?path=${encodeURIComponent(ef.sourcePath)}`);
                  if (srcRes.ok) {
                    const srcData = await srcRes.json();
                    sourceCode = srcData.content ?? '';
                  }
                  if (!sourceCode && !ef.sourcePath.match(/\.\w+$/)) {
                    const dirRes = await fetch(`/api/source-file?dir=${encodeURIComponent(ef.sourcePath)}`);
                    if (dirRes.ok) {
                      const dirData = await dirRes.json();
                      const dirFiles = dirData.files ?? [];
                      if (dirFiles.length > 0) {
                        const fileRes = await fetch(`/api/source-file?path=${encodeURIComponent(dirFiles[0])}`);
                        if (fileRes.ok) {
                          const fileData = await fileRes.json();
                          sourceCode = fileData.content ?? '';
                        }
                      }
                    }
                  }
                }
              } catch { /* ignore */ }
            }
            const targetCode = ef.content ?? '';
            const srcLines = sourceCode.split('\n').length;
            const tgtLines = targetCode.split('\n').length;
            const targetRel = normalizeEnginePath(ef.outputPath);
            const sourceRel = normalizeEnginePath(ef.sourcePath);
            const tgtLeaf = targetRel.split('/').pop() ?? `target-${idx}`;
            const rawSrcName = sourceRel.split('/').pop() ?? '';
            const srcName = rawSrcName.includes('.') ? rawSrcName : tgtLeaf.replace(/\.\w+$/, '.cbl');
            const targetFile = targetRel || `lib/migrated/${tgtLeaf}`;
            const sourceFile = sourceRel && sourceRel.includes('.')
              ? sourceRel
              : (sourceRel ? `${sourceRel.replace(/\/+$/u, '')}/${srcName}` : `repo-sources/${idx}/${srcName}`);
            return {
              id: `f${idx}`,
              sourceFile,
              targetFile,
              engineOutputPath: typeof ef.outputPath === 'string' && ef.outputPath.length > 0 ? ef.outputPath : undefined,
              sourceCode,
              targetCode,
              sourceLangId: srcLang,
              targetLangId: tgtLang,
              confidence: ef.accuracy ?? meta.accuracy ?? 0,
              linesSource: srcLines,
              linesTarget: tgtLines,
              confidenceScores: [],
              businessRules: warnFromMeta
                .filter((w) => w.trim().length > 0)
                .slice(0, 5)
                .map((w: string, i: number) => ({
                  id: `br${i}`,
                  description: w,
                  source: srcLang,
                  target: tgtLang,
                  confidence: ef.accuracy ?? meta.accuracy ?? 0,
                })),
              explanations: deriveExplanations({
                srcLang,
                tgtLang,
                linesSource: srcLines,
                linesTarget: tgtLines,
                accuracy: ef.accuracy ?? (meta as { accuracy?: number }).accuracy ?? 0,
                iterations: typeof (meta as { iterations?: number }).iterations === 'number'
                  ? ((meta as { iterations?: number }).iterations as number)
                  : 1,
                quality: (result as { quality?: { qualityIndex: number; level: string; buildReadiness?: number; semanticFidelity?: number; idiomaticity?: number } | null }).quality ?? null,
                warnings: warnFromMeta,
              }),
              reasonings: deriveReasonings({
                srcLang,
                tgtLang,
                businessRules: warnFromMeta.filter((w) => w.trim().length > 0).slice(0, 5),
                quality: (result as { quality?: { qualityIndex: number; level: string; markerApplied?: boolean } | null }).quality ?? null,
                accuracy: ef.accuracy ?? (meta as { accuracy?: number }).accuracy ?? 0,
              }),
            };
          })
        );

        disambiguateTranslationFilePaths(translationFiles);

        const engineOutAbs = engineFiles
          .map((ef: { outputPath?: string }) => ef.outputPath)
          .filter((p: unknown): p is string => typeof p === 'string' && p.length > 0);
        const { sidecars: migrationDecisionSidecars, aggregateCandidates: migrationDecisionAggregateCandidates } =
          buildMigrationDecisionPaths(engineOutAbs);

        const acc =
          typeof meta.accuracy === 'number' && Number.isFinite(meta.accuracy) ? meta.accuracy : 90;

        const wantArtifactTests =
          isPluginEnabled('test-generation', getEnabledPluginSet(cfg)) &&
          (typeof cfg.addTests === 'boolean' ? cfg.addTests : true);
        const wantArtifactDocs =
          typeof cfg.addDocs === 'boolean' ? cfg.addDocs : true;

        const totalLinesForPerf = translationFiles.reduce(
          (s: number, f: { linesSource?: number; linesTarget?: number }) => s + (f.linesSource || 0) + (f.linesTarget || 0),
          0,
        );
        const linesProcessedMeta = Number(meta.linesProcessed);
        const linesForPerf =
          Number.isFinite(linesProcessedMeta) && linesProcessedMeta > 0 ? linesProcessedMeta : totalLinesForPerf;
        const convRaw = meta.conversionTime;
        const convSec =
          typeof convRaw === 'number' && Number.isFinite(convRaw) && convRaw > 0
            ? convRaw
            : typeof convRaw === 'string'
              ? Number.parseFloat(convRaw)
              : 0;
        const perfFromRun = derivePerformanceMetricsFromRun({
          linesProcessed: linesForPerf,
          conversionTimeSec: Number.isFinite(convSec) && convSec > 0 ? convSec : 0,
          fileCount: engineFiles.length,
        });
        const legacyPerfBaseline = getLangBaseline(srcLang);
        const modernPerfBaseline = getLangBaseline(tgtLang);

        // Test artifacts come from POST /generate-tests after migration completes (no local templates).
        const unitTestFiles: ToolingArtifactFile[] = [];
        const integrationTestFiles: ToolingArtifactFile[] = [];

        const docFiles = wantArtifactDocs
          ? engineFiles.map((ef: any, i: number) => {
              const serviceName = serviceNameFromEngineFile(ef, i);
              const tf = translationFiles[i];
              const modMeta = artifactModuleMeta(ef, i, tf?.linesSource ?? 1, tf?.linesTarget ?? 1);
              const docName = `${serviceName}.md`;
              const srcLeaf = typeof ef.sourcePath === 'string' && ef.sourcePath.length > 0
                ? (ef.sourcePath.split('/').pop() ?? ef.sourcePath)
                : '';
              const tgtLeaf = typeof ef.outputPath === 'string'
                ? (ef.outputPath.split('/').pop() ?? '')
                : '';
              const body = docMarkdownForModule(serviceName, srcLang, tgtLang, srcLeaf, tgtLeaf, modMeta, analysisReport);
              return {
                id: `doc${i}`,
                name: docName,
                path: `docs/migrated/${docName}`,
                lang: 'markdown',
                lines: Math.max(1, body.split('\n').length),
                status: 'info' as const,
                description: srcLeaf
                  ? `Doc ${i + 1}: ${srcLeaf} → ${tgtLeaf || serviceName} · ${modMeta.linesSource}L→${modMeta.linesTarget}L`
                  : `Generated doc · slot ${i + 1} · ~${modMeta.linesTarget}L target`,
                code: body,
              };
            })
          : [];

        // Project Files tab: actual converted outputs from the engine (same as export zip)
        const projectFiles = engineFiles.map((ef: any, i: number) => {
          const tgtLeaf = typeof ef.outputPath === 'string'
            ? (ef.outputPath.split('/').pop() ?? `file-${i}`)
            : `file-${i}`;
          const path = `lib/migrated/${tgtLeaf}`;
          const name = tgtLeaf;
          const content = typeof ef.content === 'string' ? ef.content : '';
          const lines = Math.max(1, content.split('\n').length);
          const srcHint = typeof ef.sourcePath === 'string' && ef.sourcePath.length > 0
            ? `From ${ef.sourcePath.split('/').pop() ?? ef.sourcePath}`
            : 'Engine output';
          return {
            id: `pf${i}`,
            name,
            path,
            lang: tgtLang === 'typescript' || tgtLang === 'javascript' || tgtLang === 'node' ? 'typescript' : tgtLang,
            lines,
            status: 'info' as const,
            description: srcHint,
            code: content.length > 0 ? content : `// No content returned for ${name}\n`,
          };
        });

        const toolingFiles = [
          ...buildFallbackManifestFiles({
            tgtLang,
            srcLang,
            projectName: String((project as any)?.name ?? 'project'),
            engineFiles: engineFiles as Array<Record<string, unknown>>,
          }),
          ...buildToolingArtifacts({
            cfg: cfg as Record<string, unknown>,
            tgtLang,
            srcLang,
            projectName: String((project as any)?.name ?? 'project'),
          }),
        ];

        const testResults: Array<{
          id: string;
          name: string;
          type: 'unit' | 'integration';
          status: 'passed' | 'failed';
          duration: string;
          file: string;
        }> = [];

        // Generate security issues from engine warnings (if any)
        const warnings: string[] = warnFromMeta;
        const securityIssues = warnings.slice(0, 3).map((w: string, i: number) => ({
          id: `s${i}`,
          severity: 'info' as const,
          title: w,
          description: 'Flagged during migration analysis by Scriba Engine.',
          location: engineFiles[i]?.outputPath ?? '',
        }));

        const unitPassed = 0;
        const unitFailed = 0;
        const intPassed = 0;
        const intFailed = 0;
        const unitCases = 0;
        const intCases = 0;

        const totalTests = 0;
        const passedTests = 0;

        const analysisResults = {
          translation: {
            files: translationFiles,
            migrationDecisionSidecars,
            migrationDecisionAggregateCandidates,
            artifacts: {
              unitTestStats: {
                count: unitTestFiles.length,
                passed: unitPassed,
                failed: unitFailed,
                coverage: acc,
                cases: unitCases,
              },
              integrationTestStats: {
                count: integrationTestFiles.length,
                passed: intPassed,
                failed: intFailed,
                coverage: acc,
                cases: intCases,
              },
              docStats: {
                pages: docFiles.length > 0 ? Math.max(1, docFiles.length) : 0,
                sections: docFiles.length > 0 ? docFiles.length * 3 : 0,
              },
              archStats: {
                services: engineFiles.length,
                repositories: Math.max(1, Math.floor(engineFiles.length / 2)),
                configurations: Math.max(1, Math.floor(engineFiles.length / 4)) + toolingFiles.length,
              },
              toolingStats: { count: toolingFiles.length },
              unitTestFiles,
              integrationTestFiles,
              docFiles,
              projectFiles,
              toolingFiles,
              scribaReport,
            },
          },
          verification: {
            metrics: { overallScore: acc, syntaxScore: 100, semanticScore: acc, parityScore: acc },
            testResults,
            securityIssues,
          },
          tests: {
            metrics: { tests: totalTests, total: totalTests, passed: passedTests, failed: totalTests - passedTests, skipped: 0, coverage: acc },
          },
          testGeneration: {
            metrics: { coverage: acc, total: totalTests },
          },
          performance: {
            metrics: { throughput: modernPerfBaseline.throughput, latency: modernPerfBaseline.latency, memory: modernPerfBaseline.memoryMB },
            legacy: legacyPerfBaseline,
          },
          security: {
            metrics: {
              score: Math.min(100, Math.max(0, Math.round(Number(meta.accuracy) || acc))),
              critical: 0,
              high: 0,
              medium: 0,
              low: 0,
              info: warnings.length,
            },
          },
          documentation: {
            metrics: {
              pages: docFiles.length > 0 ? Math.max(1, docFiles.length) : 0,
              sections: docFiles.length > 0 ? docFiles.length * 3 : 0,
            },
          },
        };

        const terminalFailed = resultStatus === 'failed' || resultStatus === 'cancelled';
        const qIdx = (result as { quality?: { qualityIndex?: number } }).quality?.qualityIndex;

        // Save everything to project config (merged with existing via API)
        api.updateProject(projectId, {
          status: terminalFailed ? 'failed' : 'converting',
          ...(terminalFailed
            ? {}
            : {
                maxReachedStep: 8,
                accuracy: meta.accuracy ?? 0,
                testCoverage: acc,
                convertedFiles: engineFiles.length,
                completedAt: new Date(),
              }),
          config: {
            conversionResult: resultWithMeta,
            analysisResults,
            verification: {
              testResults,
              securityIssues,
              qualityGates: null,
            },
            completedAt: new Date().toISOString(),
            activeRunId: null,
            activeConversionId: null,
            ...(runId && !terminalFailed ? { completedRunId: runId } : {}),
          },
        })
          .then(async () => {
            if (terminalFailed) {
              setCurrentPhase('failed');
              onProjectUpdate?.();
              return;
            }

            try {
            // After the initial save, call the engine AI to generate project manifest files
            // (package.json, pom.xml, go.mod, etc.) based on what's actually in the code.
            // This replaces the import-analysis fallback that was saved above.
            if ((engineFiles as unknown[]).length > 0) {
              addLog('info', 'Generating project manifests from converted code...');
              const scaffoldInputFiles = (engineFiles as Array<{outputPath?: unknown; content?: unknown}>)
                .map(ef => ({
                  path: typeof ef.outputPath === 'string' ? ef.outputPath : '',
                  content: typeof ef.content === 'string' ? ef.content : '',
                }))
                .filter(f => f.path && f.content);

              api.engine.scaffold({
                files: scaffoldInputFiles,
                targetLanguage: tgtLang,
                sourceLanguage: srcLang,
                projectName: String((project as {name?: unknown})?.name ?? 'project'),
              })
                .then((scaffoldResult) => {
                  const scaffoldTokens = scaffoldResult.accounting?.totals?.totalTokens ?? 0;
                  if (scaffoldTokens > 0) {
                    api.recordStepProgress(projectId, 7, 'completed', 8, { actualTokens: scaffoldTokens }).catch(() => {});
                  }
                  const extLang = (p: string) => {
                    const e = p.split('.').pop()?.toLowerCase() ?? '';
                    if (e === 'json') return 'json';
                    if (e === 'toml') return 'toml';
                    if (e === 'xml') return 'xml';
                    if (e === 'yaml' || e === 'yml') return 'yaml';
                    if (e === 'ts' || e === 'tsx') return 'typescript';
                    if (e === 'go') return 'go';
                    if (e === 'rs') return 'rust';
                    if (e === 'kt' || e === 'kts') return 'kotlin';
                    if (e === 'gradle') return 'groovy';
                    if (e === 'md') return 'markdown';
                    return 'plaintext';
                  };
                  const aiManifestFiles: ToolingArtifactFile[] = scaffoldResult.files.map((f, i) => ({
                    id: `ai-manifest-${i}`,
                    name: f.path.split('/').pop() ?? f.path,
                    path: f.path,
                    lang: extLang(f.path),
                    lines: Math.max(1, f.content.split('\n').length),
                    status: 'info' as const,
                    description: `AI-generated by Scriba Engine (${srcLang} → ${tgtLang})`,
                    code: f.content,
                  }));
                  // Keep non-manifest tooling (Docker, CI/CD, ESLint, etc.) and replace manifest files
                  const otherTooling = toolingFiles.filter(f => !f.id.startsWith('manifest-'));
                  const updatedToolingFiles = [...aiManifestFiles, ...otherTooling];
                  const arObj = analysisResults as Record<string, unknown>;
                  const trObj = (arObj.translation ?? {}) as Record<string, unknown>;
                  const artObj = (trObj.artifacts ?? {}) as Record<string, unknown>;
                  const archObj = (artObj.archStats ?? {}) as Record<string, unknown>;
                  const updatedAnalysisResults = {
                    ...arObj,
                    translation: {
                      ...trObj,
                      artifacts: {
                        ...artObj,
                        toolingFiles: updatedToolingFiles,
                        toolingStats: { count: updatedToolingFiles.length },
                        archStats: { ...archObj, configurations: updatedToolingFiles.length },
                      },
                    },
                  };
                  return api.updateProject(projectId, {
                    config: { analysisResults: updatedAnalysisResults },
                  }).then(() => {
                    addLog('success', `Project manifests ready: ${aiManifestFiles.map(f => f.name).join(', ')}`);
                    // onProjectUpdate deferred to the finally block so scaffold doesn't trigger a
                    // mid-test-generation re-render on the parent.
                  });
                })
                .catch((err: unknown) => {
                  addLog('warning', `AI manifest generation failed — using import-analysis fallback. ${err instanceof Error ? err.message : ''}`);
                });
            }

            // AI test generation — awaited so migration only reaches 'completed' once tests are saved
            if (wantArtifactTests && (engineFiles as unknown[]).length > 0) {
              setSteps(prev => prev.map(s => s.id === 'test-generation' && s.status !== 'skipped' ? { ...s, status: 'running' as const, description: 'Generating unit and integration tests...' } : s));
              addLog('info', 'Generating AI unit & integration tests...');
              const testGenFiles = translationFiles.map((tf: any, i: number) => {
                const ef = (engineFiles as any[])[i];
                return {
                  outputPath: typeof ef?.outputPath === 'string' ? ef.outputPath : (tf.targetFile as string),
                  content: typeof ef?.content === 'string' ? ef.content : (tf.targetCode as string),
                  sourcePath: tf.sourceFile as string,
                  sourceContent: tf.sourceCode as string,
                };
              }).filter((f: { outputPath: string; content: string }) => f.outputPath && f.content);

              try {
                const testGenResult = await api.engine.generateTests({
                  files: testGenFiles,
                  sourceLanguage: srcLang,
                  targetLanguage: tgtLang,
                });
                const langId = langIdForTarget(tgtLang);
                const aiUnitFiles = mapEngineTestsToArtifacts(testGenResult.unit, 'unit', langId, 'ai-ut');
                const aiIntFiles = mapEngineTestsToArtifacts(testGenResult.integration, 'integration', langId, 'ai-it');
                if (aiUnitFiles.length === 0 && aiIntFiles.length === 0) {
                  addLog('warning', 'Engine returned no test files.');
                } else {
                  const unitCaseCount = sumTestCases(testGenResult.unit);
                  const intCaseCount = sumTestCases(testGenResult.integration);
                  const arObj = analysisResults as Record<string, unknown>;
                  const trObj = (arObj.translation ?? {}) as Record<string, unknown>;
                  const artObj = (trObj.artifacts ?? {}) as Record<string, unknown>;
                  const updatedTestAnalysis = {
                    ...arObj,
                    translation: {
                      ...trObj,
                      artifacts: {
                        ...artObj,
                        unitTestFiles: aiUnitFiles,
                        integrationTestFiles: aiIntFiles,
                        unitTestStats: {
                          count: aiUnitFiles.length,
                          passed: 0,
                          failed: 0,
                          coverage: 0,
                          cases: unitCaseCount,
                        },
                        integrationTestStats: {
                          count: aiIntFiles.length,
                          passed: 0,
                          failed: 0,
                          coverage: 0,
                          cases: intCaseCount,
                        },
                      },
                    },
                    tests: {
                      metrics: {
                        tests: unitCaseCount + intCaseCount,
                        total: unitCaseCount + intCaseCount,
                        passed: 0,
                        failed: 0,
                        skipped: 0,
                        coverage: 0,
                      },
                    },
                    testGeneration: {
                      metrics: {
                        coverage: 0,
                        total: aiUnitFiles.length + aiIntFiles.length,
                      },
                    },
                  };
                  await api.updateProject(projectId, {
                    config: {
                      analysisResults: updatedTestAnalysis,
                      verification: {
                        testResults: [],
                        securityIssues,
                        qualityGates: null,
                      },
                    },
                  });
                  const hints = testGenResult.warnings.length > 0 ? ` (${testGenResult.warnings.length} warning(s))` : '';
                  addLog('success', `AI tests ready: ${aiUnitFiles.length} unit, ${aiIntFiles.length} integration${hints}`);
                }
                const testGenTokens = testGenResult.accounting?.totals?.totalTokens ?? 0;
                if (testGenTokens > 0) {
                  api.recordStepProgress(projectId, 5, 'completed', 8, { actualTokens: testGenTokens }).catch(() => {});
                }
              } catch (err: unknown) {
                addLog('warning', `AI test generation failed. ${err instanceof Error ? err.message : ''}`);
              }
              setSteps(prev => prev.map(s => s.id === 'test-generation' && s.status !== 'skipped' ? { ...s, status: 'completed' as const, description: 'Unit and integration tests generated' } : s));
            }

            } finally {
              await api.updateProject(projectId, { status: 'completed' }).catch(() => {});
              setProgress(100);
              setCurrentPhase('completed');
              onProjectUpdate?.();
            }

            // Auto-run Docker sandbox after migration completes
            addLog('info', 'Launching sandbox build verification...');
            setTimeout(async () => {
              try {
                await sandboxRunnerRef.current?.(1);
              } catch (err) {
                addLog('error', `Sandbox verification error: ${err instanceof Error ? err.message : 'Unknown'}`);
              }
            }, 1500);
          })
          .catch(() => {});

        if (terminalFailed) {
          addLog('error', errFromMeta[0] ? String(errFromMeta[0]) : 'Conversion failed');
        } else if (resultStatus === 'partial') {
          addLog(
            'warning',
            qIdx != null && Number.isFinite(Number(qIdx))
              ? `Migration finished with status partial — quality index ${qIdx}, parity score ${meta.accuracy ?? '?'}%`
              : `Migration finished with status partial — parity score ${meta.accuracy ?? '?'}%`,
          );
        } else {
          addLog(
            'success',
            qIdx != null && Number.isFinite(Number(qIdx))
              ? `Migration completed — quality index ${qIdx}, parity score ${meta.accuracy ?? '?'}%`
              : `Migration completed — parity score ${meta.accuracy ?? '?'}%`,
          );
        }
        es.onerror = null;
        es.close();
        resolve();
      });

      es.addEventListener('error', (e: MessageEvent) => {
        if (streamSucceeded) return;
        const d = JSON.parse((e as any).data || '{}');
        if (d.code === 'BudgetExceeded') {
          setBudgetExceeded({ message: d.message });
        }
        addLog('error', d.message || 'Stream error');
        setCurrentPhase('failed');
        hasReconnectedRef.current = false;
        api.updateProject(projectId, { status: 'failed', config: { activeRunId: null, activeConversionId: null } }).then(() => {
          onProjectUpdate?.();
        }).catch(() => {});
        enqueueMigrationNotify(projectId, 'failure', d.message || 'Stream error');
        es.close();
        resolve();
      });

      es.onerror = () => {
        if (streamSucceeded) return;
        setEngineAlive(false);

        // Attempt one SSE resume with Last-Event-ID before treating as failure
        const lastId = lastSseEventIdRef.current;
        if (lastId && sseReconnectAttemptRef.current === 0 && runId) {
          sseReconnectAttemptRef.current = 1;
          addLog('info', 'Connection lost — resuming stream...');
          es.close();
          const resumed = api.engine.streamRun(runId, lastId);
          esRef.current = resumed;
          resumed.onopen = () => setEngineAlive(true);
          // Copy all listeners by re-running subscribeToConversion is too heavy;
          // instead just mark the outer promise as still running — the resumed ES
          // inherits the same outer promise via closure and the listeners are re-attached
          // by the recursive call below.
          resumed.onerror = () => {
            if (streamSucceeded) return;
            setEngineAlive(false);
            addLog('info', 'Connection to engine lost after resume attempt — attempting poll recovery');
            resumed.close();
            // Poll fallback with 409 retry loop (ML01 §3.6)
            void pollResult(runId);
          };
          return;
        }

        es.close();
        if (runId) {
          // Poll fallback with 409 retry loop (ML01 §3.6)
          addLog('info', 'Connection to engine lost — attempting poll recovery');
          void pollResult(runId);
        } else {
          addLog('error', 'Connection to engine lost');
          setCurrentPhase('failed');
          hasReconnectedRef.current = false;
          api.updateProject(projectId, { status: 'failed', config: { activeRunId: null, activeConversionId: null } }).then(() => {
            onProjectUpdate?.();
          }).catch(() => {});
          enqueueMigrationNotify(projectId, 'failure', 'Connection to engine lost');
          resolve();
        }
      };
    });
  };

  const startMigration = async () => {
    if (!costApprovalDone) return;
    hasReconnectedRef.current = false;
    migrationStartTimeRef.current = Date.now();
    etaRef.current = null;
    const initialCfg = parseProjectConfig(project);
    const rawSrcEarly = (project as any)?.source_language || (project as any)?.sourceLanguage;
    const srcLangEarly = typeof rawSrcEarly === 'string' && rawSrcEarly.trim() ? normalizeProjectLang(rawSrcEarly) : '';
    initializeSteps(getEnabledPluginSet(initialCfg), srcLangEarly);
    progressRef.current = 0;
    setProgress(0);
    setLogs([]);
    logIdRef.current = 0;
    setLiveCostUsd(0);
    setQueuePosition(null);
    setDetectedProfile(null);

    // Smooth progress ticker — nudges displayed % slightly every second so the bar never looks frozen
    if (progressTickerRef.current) clearInterval(progressTickerRef.current);
    progressTickerRef.current = setInterval(() => {
      setProgress(prev => {
        if (prev >= 99) return prev;
        const bump = prev < 30 ? 0.08 : prev < 70 ? 0.05 : 0.02;
        const next = Math.min(prev + bump, 99);
        progressRef.current = next;
        return next;
      });
    }, 1000);
    if (projectId) api.clearLogs(projectId).catch(() => {});
    setConversionResult(null);
    lastStructuralEngineFailureRef.current = undefined;
    setCurrentPhase('validation');
    addLog('info', `Starting migration for project ${projectId}`);

    // Update project status to 'converting' in DB
    try {
      await api.updateProject(projectId, { status: 'converting' });
    } catch { /* non-blocking */ }

    // Reload project so wizard `config.customRules` (and other DB updates) are not missed due to stale props.
    let projectForEngine: typeof project = project;
    try {
      const data = await api.getProject(projectId);
      if (data?.project) projectForEngine = data.project as typeof project;
    } catch {
      /* use prop */
    }

    // Determine source/target from project metadata
    // sourceLanguage stored as engine ID (e.g. 'cobol') or display name — normalise to lowercase
    const rawSrc = (projectForEngine as any)?.source_language || (projectForEngine as any)?.sourceLanguage;
    const rawTgt = (projectForEngine as any)?.target_language || (projectForEngine as any)?.targetLanguage;
    // Strip space/hyphen version suffixes: 'COBOL-85' → 'cobol', 'Java 22' → 'java', 'TypeScript-5' → 'typescript'
    const srcLang = typeof rawSrc === 'string' && rawSrc.trim() ? normalizeProjectLang(rawSrc) : '';
    const tgtLang = typeof rawTgt === 'string' && rawTgt.trim() ? normalizeProjectLang(rawTgt) : '';
    if (!srcLang || !tgtLang) {
      addLog('error', 'This project has no source or target language set. Edit the project and choose both languages, then try again.');
      setCurrentPhase('failed');
      api.updateProject(projectId, { status: 'failed' }).catch(() => {});
      enqueueMigrationNotify(projectId, 'failure', 'Missing source or target language');
      return;
    }
    const cfg = parseProjectConfig(projectForEngine);
    const repoUrl = (projectForEngine as any)?.repo_url || (projectForEngine as any)?.repoUrl || '';
    const repoRef = typeof cfg.branch === 'string' && cfg.branch.trim() ? cfg.branch.trim() : undefined;
    const srcPath = (projectForEngine as any)?.source_path || cfg.sourcePath || '';
    let conversionId: string | null = null;
    let runId: string | null = null;

    if (engineOnline === false) {
      addLog('error', 'Scriba engine is not reachable. Please ensure the engine is running on port 3100.');
      setCurrentPhase('failed');
      api.updateProject(projectId, { status: 'failed' }).catch(() => {});
      enqueueMigrationNotify(projectId, 'failure', 'Scriba engine not reachable');
      return;
    }
    if (engineOnline === null) {
      addLog('warning', 'Waiting for engine health check...');
      // Wait a moment for health check to complete
      await new Promise(r => setTimeout(r, 1000));
      if (engineOnline === false) {
        addLog('error', 'Scriba engine is not reachable.');
        setCurrentPhase('failed');
        api.updateProject(projectId, { status: 'failed' }).catch(() => {});
        enqueueMigrationNotify(projectId, 'failure', 'Scriba engine not reachable after health wait');
        return;
      }
    }

    try {
      // Resolve enabled/disabled plugins first — gates some downstream options.
      const enabledPluginSet = getEnabledPluginSet(cfg);
      const disabledPlugins = PIPELINE_PLUGINS
        .filter(p => !isPluginEnabled(p.id, enabledPluginSet))
        .map(p => p.id);

      const customRules = mergeRules(extractCustomRules(cfg), cfg);
      const maxIterations =
        typeof cfg.maxIterations === 'number' && Number.isFinite(cfg.maxIterations)
          ? cfg.maxIterations
          : 3;
      const targetAccuracyRaw =
        typeof cfg.targetAccuracy === 'number' && Number.isFinite(cfg.targetAccuracy)
          ? cfg.targetAccuracy
          : undefined;
      const targetAccuracy =
        targetAccuracyRaw !== undefined
          ? Math.min(100, Math.max(0, Math.round(targetAccuracyRaw)))
          : 90;
      const preserveComments =
        typeof cfg.preserveComments === 'boolean' ? cfg.preserveComments : true;
      /** Disabled test-generation plugin suppresses test artifact generation regardless of addTests. */
      const includeTests =
        !disabledPlugins.includes('test-generation') &&
        (typeof cfg.addTests === 'boolean' ? cfg.addTests : true);
      const includePatterns =
        typeof cfg.includePatterns === 'string' && cfg.includePatterns.trim().length > 0
          ? cfg.includePatterns.trim()
          : undefined;
      const excludePatterns =
        typeof cfg.excludePatterns === 'string' && cfg.excludePatterns.trim().length > 0
          ? cfg.excludePatterns.trim()
          : undefined;

      const qualityLevelRaw =
        typeof cfg.qualityLevel === 'number' && Number.isFinite(cfg.qualityLevel) ? cfg.qualityLevel : 0;
      const qualityLevel = Math.min(3, Math.max(0, Math.round(qualityLevelRaw)));
      // Omit when unset so scriba-engine env defaults apply (repair ON, stages via SCRIBA_STAGES).
      const useStages = typeof cfg.useStages === 'boolean' ? cfg.useStages : undefined;
      const useRepair = typeof cfg.useRepair === 'boolean' ? cfg.useRepair : undefined;

      const additionalSourceLanguages = Array.isArray(cfg.additionalSourceLanguages)
        ? (cfg.additionalSourceLanguages as string[]).filter(Boolean)
        : [];
      const sourceFramework =
        typeof cfg.sourceFramework === 'string' && cfg.sourceFramework.trim().length > 0
          ? cfg.sourceFramework.trim()
          : undefined;

      // Fetch stored GitHub token so the engine can clone private repos.
      let repoAccessToken: string | undefined;
      if (repoUrl) {
        const patRes = await fetch('/api/github/pat').catch(() => null);
        if (patRes?.ok) {
          const patData = await patRes.json().catch(() => ({})) as { token?: string | null };
          if (typeof patData.token === 'string' && patData.token.length > 0) {
            repoAccessToken = patData.token;
          }
        }
      }

      const useLegacy = needsLegacyConvert(cfg, {
        repoUrl: (projectForEngine as { repoUrl?: string; repo_url?: string; sourcePath?: string; source_path?: string })?.repoUrl
          ?? (projectForEngine as { repo_url?: string })?.repo_url,
        sourcePath: (projectForEngine as { sourcePath?: string; source_path?: string })?.sourcePath
          ?? (projectForEngine as { source_path?: string })?.source_path,
      });

      if (!useLegacy) {
        addLog('info', 'Preparing source bundle for engine...');
        const prep = await api.engine.prepareUpload(projectId);
        if (!prep.reused && prep.fileCount > 0) {
          addLog('info', `Uploaded ${prep.fileCount} source files (${Math.round(prep.totalBytes / 1024)} KB)`);
        } else if (prep.reused) {
          addLog('info', 'Using cached source bundle upload');
        }

        const runRes = await api.engine.startRun(buildStartRunOptions(cfg, srcLang, tgtLang, prep.uploadId));
        runId = runRes.runId;
        conversionId = runRes.conversionId;
        addLog('info', `Run queued — runId: ${runId}, conversionId: ${conversionId}`);
      } else {
        addLog('info', 'Using legacy /convert path (dependency mapping, plugin overrides, or local source path)...');

        const VALID_PROFILES = ['auto', 'batch', 'online', 'library', 'utility', 'mixed'];
        const cfgProfile = typeof cfg.projectProfile === 'string' ? cfg.projectProfile.trim() : '';
        const convertProfile = VALID_PROFILES.includes(cfgProfile) && cfgProfile !== 'auto' ? cfgProfile : undefined;
        const convertIntent =
          cfg.intent === 'compat-strict' || cfg.intent === 'parity' || cfg.intent === 'modernize'
            ? cfg.intent as 'compat-strict' | 'parity' | 'modernize'
            : undefined;
        const res = await api.engine.startConvert({
          sourceLanguage: srcLang,
          ...(additionalSourceLanguages.length > 0 ? { additionalSourceLanguages } : {}),
          targetLanguage: tgtLang,
          sourcePath: srcPath || '',
          ...(repoUrl ? { repoUrl } : {}),
          ...(repoRef ? { ref: repoRef } : {}),
          ...(repoAccessToken ? { accessToken: repoAccessToken } : {}),
          ...(sourceFramework ? { sourceFramework } : {}),
          ...((convertProfile || convertIntent) ? { project: { ...(convertProfile ? { profile: convertProfile as 'batch' | 'online' | 'library' | 'utility' | 'mixed' } : {}), ...(convertIntent ? { intent: convertIntent } : {}) } } : {}),
          options: {
            maxIterations,
            targetAccuracy,
            preserveComments,
            includeTests,
            qualityLevel,
            ...(useStages !== undefined ? { useStages } : {}),
            ...(useRepair !== undefined ? { useRepair } : {}),
            ...(disabledPlugins.length > 0 ? { disabledPlugins } : {}),
            ...(customRules ? { customRules } : {}),
            ...(includePatterns ? { includePatterns } : {}),
            ...(excludePatterns ? { excludePatterns } : {}),
            ...(Array.isArray(cfg.dependencyMapping) && (cfg.dependencyMapping as unknown[]).length > 0 ? {
              dependencyMapping: (cfg.dependencyMapping as Array<{ id: string; source: string; target: string; version: string; notes: string; status: 'auto' | 'modified' | 'added' | 'removed' }>)
                .filter(e => e.status !== 'removed') as Array<{ id: string; source: string; target: string; version: string; notes: string; status: 'auto' | 'modified' | 'added' }>,
            } : {}),
          },
        });
        conversionId = res.conversionId;
        addLog('info', `Conversion started — ID: ${conversionId}`);
      }
    } catch (err) {
      const e = err as { status?: number; retryAfter?: number; quotaPayload?: Record<string, unknown> };
      if (e.status === 429) {
        const qp = e.quotaPayload ?? {};
        setQuotaError({
          reason: qp.reason as 'rate-limit' | 'concurrency-cap' | undefined,
          observed: qp.observed as { runs_last_hour?: number; in_flight?: number } | undefined,
          limits: qp.limits as { runs_per_hour?: number; max_concurrent?: number } | undefined,
          retry_after_s: (qp.retry_after_s as number | undefined) ?? e.retryAfter ?? 60,
        });
        setCurrentPhase('failed');
        if (progressTickerRef.current) { clearInterval(progressTickerRef.current); progressTickerRef.current = null; }
        return;
      }
      addLog('error', `Failed to start conversion: ${err}`);
      setCurrentPhase('failed');
      api.updateProject(projectId, { status: 'failed' }).catch(() => {});
      enqueueMigrationNotify(projectId, 'failure', `Failed to start conversion: ${err}`);
      return;
    }

    // Persist handles so we can reconnect if the user navigates away
    api.updateProject(projectId, {
      config: {
        ...(runId ? { activeRunId: runId } : {}),
        activeConversionId: conversionId,
        ...(runId ? {} : { activeRunId: null }),
      },
    }).catch(() => {});

    await subscribeToConversion(conversionId!, srcLang, tgtLang, cfg, runId);
    // Migration finished (success, failure, or cancel) — stop the nudge ticker
    if (progressTickerRef.current) { clearInterval(progressTickerRef.current); progressTickerRef.current = null; }
  };

  const handleRetry = () => {
    hasReconnectedRef.current = false;
    sseReconnectAttemptRef.current = 0;
    lastSseEventIdRef.current = undefined;
    esRef.current?.close();
    if (progressTickerRef.current) { clearInterval(progressTickerRef.current); progressTickerRef.current = null; }
    setCurrentPhase('pre-analysis');
    setProgress(0);
    setLogs([]);
    lastStructuralEngineFailureRef.current = undefined;
    setConversionResult(null);
    setActiveConversionId(null);
    setActiveRunId(null);
    setLiveUsage(null);
    setEngineAlive(null);
    setQuotaError(null);
    setQuotaCountdown(0);
    setBudgetExceeded(null);
    setRunLost(false);
    setLiveCostUsd(0);
    setQueuePosition(null);
    setDetectedProfile(null);
    const retryCfg = parseProjectConfig(project);
    const rawSrcRetry = (project as any)?.source_language || (project as any)?.sourceLanguage;
    const srcLangRetry = typeof rawSrcRetry === 'string' && rawSrcRetry.trim() ? normalizeProjectLang(rawSrcRetry) : '';
    initializeSteps(getEnabledPluginSet(retryCfg), srcLangRetry);
  };

  // ── Sandbox auto-run & AI repair loop ─────────────────────────────────────

  const fixSandboxErrors = useCallback(async (
    attempt: number,
    appendSandboxLog: (l: { line: string; stream: 'stdout' | 'stderr'; level: string }) => void
  ) => {
    appendSandboxLog({ line: `[repair] Engine analyzing build errors — attempt ${attempt + 1} of 3...`, stream: 'stdout', level: 'info' });
    const errorLines = sandboxLogsRef.current
      .filter(l => l.level === 'error' || (l.stream === 'stderr' && /error|exception|failed/i.test(l.line)))
      .map(l => l.line)
      .slice(0, 50);
    try {
      const res = await fetch(`/api/conversions/${projectId}/sandbox/fix`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ errors: errorLines }),
      });
      const result = await res.json() as { patchedCount?: number; error?: string };
      if (!res.ok || result.error) throw new Error(result.error ?? `HTTP ${res.status}`);
      appendSandboxLog({ line: `[repair] Applied patches to ${result.patchedCount ?? 0} file(s). Re-running build...`, stream: 'stdout', level: 'info' });
      await sandboxRunnerRef.current?.(attempt + 1);
    } catch (err) {
      appendSandboxLog({ line: `[repair] Auto-fix failed: ${err instanceof Error ? err.message : 'Unknown error'}`, stream: 'stderr', level: 'error' });
      setSandboxPhase('failed');
    }
  }, [projectId]);

  const runSandboxFn = useCallback(async (attempt: number) => {
    setSandboxPhase('running');
    setSandboxAttempt(attempt);
    sandboxLogsRef.current = [];
    sandboxIdRef.current = 0;
    setSandboxLogs([]);
    setSandboxExitCode(null);
    setSandboxDuration(null);
    setSandboxElapsed(0);
    sandboxStartMsRef.current = Date.now();

    // Live elapsed timer — ticks every second while sandbox is running
    if (sandboxElapsedTimerRef.current) clearInterval(sandboxElapsedTimerRef.current);
    sandboxElapsedTimerRef.current = setInterval(() => {
      if (sandboxStartMsRef.current != null) {
        setSandboxElapsed(Math.floor((Date.now() - sandboxStartMsRef.current) / 1000));
      }
    }, 1000);

    const appendSandboxLog = (log: { line: string; stream: 'stdout' | 'stderr'; level: string; elapsed?: string }) => {
      const entry = { id: String(++sandboxIdRef.current), ...log };
      sandboxLogsRef.current = [...sandboxLogsRef.current, entry];
      setSandboxLogs([...sandboxLogsRef.current]);
      requestAnimationFrame(() => sandboxLogEndRef.current?.scrollIntoView({ behavior: 'smooth' }));
    };

    try {
      appendSandboxLog({ line: '[sandbox] Starting Docker sandbox...', stream: 'stdout', level: 'info' });
      const response = await fetch(`/api/conversions/${projectId}/sandbox`, { method: 'POST' });
      if (!response.body) throw new Error('No response stream from server');

      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let buf = '';
      let finalSuccess = false;
      let gotDone = false;

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        buf += decoder.decode(value, { stream: true });
        const blocks = buf.split('\n\n');
        buf = blocks.pop() ?? '';
        for (const block of blocks) {
          if (!block.trim()) continue;
          const lines = block.split('\n');
          let event = 'message';
          let data = '';
          for (const l of lines) {
            if (l.startsWith('event: ')) event = l.slice(7).trim();
            if (l.startsWith('data: ')) data = l.slice(6);
          }
          if (!data) continue;
          try {
            const parsed = JSON.parse(data) as Record<string, unknown>;
            if (event === 'log') {
              appendSandboxLog({
                line: String(parsed.line ?? ''),
                stream: (parsed.stream === 'stderr' ? 'stderr' : 'stdout') as 'stdout' | 'stderr',
                level: String(parsed.level ?? 'info'),
                elapsed: typeof parsed.elapsed === 'string' ? parsed.elapsed : undefined,
              });
            } else if (event === 'done') {
              gotDone = true;
              finalSuccess = parsed.success === true;
              setSandboxExitCode(typeof parsed.exitCode === 'number' ? parsed.exitCode : null);
              setSandboxDuration(typeof parsed.duration === 'number' ? parsed.duration : null);
            } else if (event === 'error') {
              appendSandboxLog({ line: `[sandbox] ${String(parsed.message ?? 'Unknown error')}`, stream: 'stderr', level: 'error' });
            }
          } catch { /* ignore bad JSON */ }
        }
      }

      if (sandboxElapsedTimerRef.current) { clearInterval(sandboxElapsedTimerRef.current); sandboxElapsedTimerRef.current = null; }

      if (!gotDone) {
        setSandboxPhase('failed');
        return;
      }

      if (finalSuccess) {
        setSandboxPhase('success');
        addLog('success', 'Sandbox build verification passed — no errors detected.');
      } else if (attempt < 3) {
        await fixSandboxErrors(attempt, appendSandboxLog);
      } else {
        setSandboxPhase('failed');
        addLog('error', 'Sandbox build verification failed after 3 attempts. See sandbox panel for details.');
      }
    } catch (err) {
      if (sandboxElapsedTimerRef.current) { clearInterval(sandboxElapsedTimerRef.current); sandboxElapsedTimerRef.current = null; }
      setSandboxPhase('failed');
      const entry = { id: String(++sandboxIdRef.current), line: `[sandbox] ${err instanceof Error ? err.message : 'Connection error'}`, stream: 'stderr' as const, level: 'error' };
      sandboxLogsRef.current = [...sandboxLogsRef.current, entry];
      setSandboxLogs([...sandboxLogsRef.current]);
      addLog('error', `Sandbox verification failed: ${err instanceof Error ? err.message : 'Connection error'}`);
    }
  }, [projectId, fixSandboxErrors, addLog]);

  sandboxRunnerRef.current = runSandboxFn;

  // ── End sandbox ────────────────────────────────────────────────────────────

  const handleCancelConfirm = async () => {
    setCancelConfirm(false);
    if (!activeRunId && !activeConversionId) return;
    try {
      if (activeRunId) {
        await api.engine.cancelRun(activeRunId);
      } else if (activeConversionId) {
        await api.engine.cancelConversion(activeConversionId);
      }
      addLog('warning', 'Cancellation signal sent — waiting for engine to acknowledge...');
    } catch (err) {
      addLog('error', `Cancel request failed: ${err}`);
    }
  };

  /** §3 NEXT — download MIGRATION-DECISIONS.md from engine output (via source-file proxy). */
  const handleDownloadDecisionLog = async () => {
    const filePaths = ((conversionResult as { files?: Array<{ path?: string }> })?.files ?? [])
      .map(f => f?.path)
      .filter((p): p is string => typeof p === 'string' && p.length > 0);
    const { aggregateCandidates } = buildMigrationDecisionPaths(filePaths);
    for (const candidate of aggregateCandidates) {
      try {
        const res = await fetch(`/api/source-file?path=${encodeURIComponent(candidate)}`);
        if (!res.ok) continue;
        const data = await res.json() as { content?: string };
        if (!data.content) continue;
        const blob = new Blob([data.content], { type: 'text/markdown' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'MIGRATION-DECISIONS.md';
        a.click();
        URL.revokeObjectURL(url);
        return;
      } catch {
        continue;
      }
    }
  };

  const completedSteps = steps.filter(s => s.status === 'completed').length;
  const activeStepCount = steps.filter(s => s.status !== 'skipped').length;
  const currentStep = steps.find(s => s.status === 'running');
  const lastCompletedStep = [...steps].reverse().find(s => s.status === 'completed');
  const displayStep = currentStep ?? (currentPhase !== 'pre-analysis' && currentPhase !== 'completed' && currentPhase !== 'failed' ? lastCompletedStep : null);

  return (
    <div className="max-w-6xl mx-auto space-y-6">
      {/* Cancel confirmation modal */}
      <AnimatePresence>
        {cancelConfirm && (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
          >
            <motion.div
              initial={{ scale: 0.92, opacity: 0 }}
              animate={{ scale: 1, opacity: 1 }}
              exit={{ scale: 0.92, opacity: 0 }}
              className="glass rounded-xl p-6 max-w-sm w-full mx-4 border border-red-500/30"
            >
              <h3 className="text-base font-semibold text-foreground mb-2">Cancel migration?</h3>
              <p className="text-sm text-muted mb-5 leading-relaxed">
                The engine will stop at the next checkpoint. Partially converted files may still be available. You can restart with a new run at any time.
              </p>
              <div className="flex gap-3">
                <button
                  onClick={handleCancelConfirm}
                  className="flex-1 py-2 rounded-lg bg-red-500/15 border border-red-500/40 text-red-400 text-xs font-semibold hover:bg-red-500/25 transition-colors cursor-pointer"
                >
                  Yes, cancel
                </button>
                <button
                  onClick={() => setCancelConfirm(false)}
                  className="flex-1 py-2 rounded-lg border border-border text-xs font-semibold hover:bg-surface-light transition-colors cursor-pointer"
                >
                  Keep running
                </button>
              </div>
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>

      {/* Header */}
      <motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }}>
        <div className="flex items-center gap-3 flex-wrap">
          <h2 className="text-2xl font-bold text-foreground">Migration Process</h2>
          {engineOnline === true && <span className="text-[10px] px-2 py-0.5 rounded-full bg-success/15 text-success font-medium">Engine Online</span>}
          {engineOnline === false && <span className="text-[10px] px-2 py-0.5 rounded-full bg-red-500/15 text-red-400 font-medium">Engine Offline</span>}
          {engineOnline === null && <span className="text-[10px] px-2 py-0.5 rounded-full bg-surface text-muted font-medium">Checking...</span>}
          {engineAlive === true && (currentPhase === 'migration' || currentPhase === 'validation' || currentPhase === 'verification') && (
            <span className="text-[10px] px-2 py-0.5 rounded-full bg-accent/15 text-accent-light font-medium flex items-center gap-1">
              <span className="w-1.5 h-1.5 rounded-full bg-accent-light animate-pulse inline-block" /> Live
            </span>
          )}
          {(currentPhase === 'migration' || currentPhase === 'validation' || currentPhase === 'verification') && (activeRunId || activeConversionId) && (
            <button
              onClick={() => setCancelConfirm(true)}
              className="ml-auto text-[10px] px-3 py-1 rounded-md border border-red-500/40 text-red-400 hover:bg-red-500/10 transition-colors cursor-pointer flex items-center gap-1.5"
            >
              <X className="w-3 h-3" /> Cancel
            </button>
          )}
        </div>
        <p className="text-sm text-muted mt-1">Comprehensive migration workflow with detailed analysis and verification</p>
      </motion.div>

      {/* Cost Approval Gate — shown once before migration can start */}
      {currentPhase === 'pre-analysis' && !costApprovalDone && (
        <CostApprovalGate
          project={project ?? null}
          onConfirm={handleCostApproval}
          onReject={() => onNavigate('migration-strategy', projectId)}
        />
      )}

      {/* Empty State - Show after cost is approved but before migration starts */}
      {currentPhase === 'pre-analysis' && costApprovalDone && (
        <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="glass-light rounded-lg p-12 text-center">
          <div className="w-20 h-20 rounded-full gradient-accent flex items-center justify-center mx-auto mb-6">
            <Rocket className="w-10 h-10 text-white" />
          </div>
          <h3 className="text-xl font-semibold text-foreground mb-3">Ready to Start Migration</h3>
          <p className="text-sm text-muted mb-6 max-w-md mx-auto">
            Click the button below to begin the migration process. The system will analyze your source code, translate it to the target language, and validate the results.
          </p>
          {!project?.repoUrl && (() => {
            const cfg = parseProjectConfig(project);
            const existingUploadId = cfg.uploadId as string | undefined;
            const existingFolderName = cfg.folderName as string | undefined;
            const existingMeta = cfg.uploadMeta as { fileCount?: number; totalBytes?: number } | undefined;
            if (existingUploadId) {
              return (
                <div className="mb-6 max-w-md mx-auto text-left glass-light rounded-lg p-4 border border-success/30">
                  <div className="flex items-center gap-2 mb-1">
                    <HardDrive className="w-4 h-4 text-success flex-shrink-0" />
                    <p className="text-xs font-medium text-success">Source folder ready</p>
                  </div>
                  <p className="text-xs text-foreground font-mono">{existingFolderName || existingUploadId}</p>
                  {existingMeta?.fileCount != null && (
                    <p className="text-[10px] text-muted mt-0.5">
                      {existingMeta.fileCount} files · {Math.round((existingMeta.totalBytes ?? 0) / 1024)} KB
                    </p>
                  )}
                </div>
              );
            }
            return (
              <div className="mb-6 max-w-md mx-auto text-left glass-light rounded-lg p-4 border border-border">
                <p className="text-xs text-muted mb-2">No Git repository linked — upload a source folder instead:</p>
                <label className="inline-flex items-center gap-2 px-4 py-2 rounded-md border border-accent/40 text-accent-light text-xs font-medium cursor-pointer hover:bg-accent/10 transition-colors">
                  <FolderPlus className="w-4 h-4" />
                  Select project folder
                  <input
                    type="file"
                    className="hidden"
                    // @ts-expect-error webkitdirectory is non-standard but supported in Chromium
                    webkitdirectory=""
                    directory=""
                    multiple
                    onChange={handleBundleUpload}
                  />
                </label>
                {bundleUploadStatus && (
                  <p className="text-[10px] text-muted mt-2">{bundleUploadStatus}</p>
                )}
              </div>
            );
          })()}
          <button
            onClick={startMigration}
            className="px-8 py-3 rounded-lg gradient-accent text-white text-sm font-semibold hover:opacity-90 transition-opacity flex items-center justify-center gap-2 cursor-pointer mx-auto glow-accent"
          >
            <Play className="w-5 h-5" /> Start Migration
          </button>
        </motion.div>
      )}

      {/* Project Summary Banner - Show once cost is approved */}
      {(currentPhase !== 'pre-analysis' || costApprovalDone) && (
        <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="glass-light rounded-lg p-4">
          <div className="flex items-center justify-between">
            <div className="flex items-center gap-4">
              <div className="w-12 h-12 rounded-lg gradient-accent flex items-center justify-center">
                <FolderPlus className="w-6 h-6 text-white" />
              </div>
              <div>
                <p className="text-sm font-semibold text-foreground flex items-center gap-2">
                  {project?.name || `Project ${projectId}`}
                  {detectedProfile && (
                    <span className="text-[9px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded bg-accent/15 text-accent-light border border-accent/25">
                      {detectedProfile}
                    </span>
                  )}
                </p>
                <p className="text-xs text-muted">
                  {(project?.sourceLanguage?.trim() || '—') + ' → ' + (project?.targetLanguage?.trim() || '—')}
                  {project?.repoUrl ? ` • ${project.repoUrl.replace(/^https?:\/\/github\.com\//, '')}` : ''}
                </p>
              </div>
            </div>
            <div className="flex items-center gap-4 text-xs">
              <div className="flex items-center gap-2">
                <Clock className="w-4 h-4 text-muted" />
                <span className="text-muted">Progress:</span>
                <span className="text-foreground">{progress.toFixed(0)}%</span>
              </div>
              <div className="flex items-center gap-2">
                <Activity className="w-4 h-4 text-muted" />
                <span className="text-muted">Steps:</span>
                <span className="text-foreground">{completedSteps}/{activeStepCount}</span>
              </div>
            </div>
          </div>
        </motion.div>
      )}

      {/* Queue position banner — shown prominently when job is waiting (progress still 0) */}
      <AnimatePresence>
        {queuePosition && progress === 0 && currentPhase === 'migration' && (
          <motion.div
            key="queue-banner"
            initial={{ opacity: 0, y: -8 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -8 }}
            className="glass-light rounded-lg p-4 border border-amber-400/30 flex items-center gap-3 mb-4"
          >
            <Loader2 className="w-4 h-4 text-amber-400 animate-spin shrink-0" />
            <div>
              <p className="text-xs font-semibold text-amber-400">
                Queued — position {queuePosition.position} of {queuePosition.depth}
              </p>
              <p className="text-[10px] text-muted mt-0.5">
                The engine is processing earlier jobs. Your run will start automatically.
              </p>
            </div>
          </motion.div>
        )}
      </AnimatePresence>

      {/* Phase Progress - Always shown once cost is approved */}
      {(currentPhase !== 'pre-analysis' || costApprovalDone) && (
        <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.05 }} className="glass-light rounded-lg p-4">
          <div className="flex items-center justify-between mb-3">
            <h4 className="text-xs font-medium text-foreground uppercase tracking-wider">Migration Phases</h4>
            <span className="text-xs text-muted">{completedSteps}/{activeStepCount} steps completed</span>
          </div>
          <div className="relative">
            <div className="h-2 bg-surface rounded-full overflow-hidden">
              <motion.div
                initial={{ width: 0 }}
                animate={{ width: `${progress}%` }}
                transition={{ duration: 0.5 }}
                className={`h-full gradient-accent${progress > 0 && progress < 100 ? ' animate-pulse' : ''}`}
              />
            </div>
          </div>
          <div className="flex justify-between mt-3">
            {[
              { id: 'validation', label: 'Validation', icon: Shield },
              { id: 'pre-flight', label: 'Pre-flight', icon: Activity },
              { id: 'migration', label: 'Migration', icon: Zap },
              { id: 'verification', label: 'Verification', icon: CheckCircle2 },
            ].map((phase, index) => {
              const isCompleted = progress > (index + 1) * 25;
              const isCurrent = currentPhase === phase.id;
              const Icon = phase.icon;
              return (
                <div key={phase.id} className="flex flex-col items-center gap-1">
                  <div className={`w-8 h-8 rounded-full flex items-center justify-center ${
                    isCompleted ? 'bg-success text-white' : isCurrent ? 'bg-accent text-white' : 'bg-surface text-muted'
                  }`}>
                    <Icon className="w-4 h-4" />
                  </div>
                  <span className={`text-[10px] ${isCompleted || isCurrent ? 'text-foreground' : 'text-muted'}`}>{phase.label}</span>
                </div>
              );
            })}
          </div>
        </motion.div>
      )}

      {/* Main Content Grid - Show only while migration is actively running or completed */}
      {currentPhase !== 'pre-analysis' && (
        <div className="grid grid-cols-3 gap-6">
          {/* Left Column - Steps */}
          <div className="col-span-2 space-y-6">
            {/* Current Step Highlight */}
            {displayStep && (
              <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="glass rounded-lg p-4 border border-accent/30 glow-accent">
                <div className="flex items-center gap-3 mb-3">
                  <div className="w-10 h-10 rounded-lg bg-accent/15 flex items-center justify-center">
                    {currentStep
                      ? <Loader2 className="w-5 h-5 text-accent-light animate-spin" />
                      : <CheckCircle2 className="w-5 h-5 text-accent-light" />
                    }
                  </div>
                  <div>
                    <p className="text-sm font-semibold text-foreground">{displayStep.name}</p>
                    <p className="text-xs text-muted">{displayStep.description}</p>
                  </div>
                </div>
                <div className="grid grid-cols-3 gap-3">
                  <div className="glass-light rounded-md p-3">
                    <p className="text-[10px] text-muted uppercase tracking-wider">Progress</p>
                    <p className="text-lg font-bold text-accent-light">{progress.toFixed(1)}%</p>
                  </div>
                  <div className="glass-light rounded-md p-3">
                    <p className="text-[10px] text-muted uppercase tracking-wider">ETA</p>
                    <p className="text-lg font-bold text-foreground">{calculateETA()}</p>
                  </div>
                  <div className="glass-light rounded-md p-3">
                    <p className="text-[10px] text-muted uppercase tracking-wider">Resources</p>
                    <p className="text-lg font-bold text-success">OK</p>
                  </div>
                </div>
              </motion.div>
            )}

            {/* Migration Steps */}
            <div className="glass rounded-lg p-4">
              <div className="flex items-center justify-between mb-4">
                <h4 className="text-xs font-medium text-foreground uppercase tracking-wider flex items-center gap-2">
                  <Layers className="w-4 h-4" />
                  Migration Steps
                </h4>
                <button
                  onClick={() => setShowDetails(!showDetails)}
                  className="text-[10px] text-muted hover:text-foreground transition-colors cursor-pointer"
                >
                  {showDetails ? 'Hide' : 'Show'} Details
                </button>
              </div>

              <div className="space-y-2">
                {steps.map((step, index) => {
                  const isActive = step.id === displayStep?.id;
                  const StatusIcon = step.status === 'completed' ? CheckCircle2 : step.status === 'running' ? Loader2 : step.status === 'failed' ? X : step.status === 'skipped' ? AlertTriangle : Activity;
                  const statusColor = step.status === 'completed' ? 'text-success' : step.status === 'running' ? 'text-accent-light animate-spin' : step.status === 'failed' ? 'text-danger' : step.status === 'skipped' ? 'text-amber-400' : 'text-muted';
                  const bgColor = step.status === 'completed' ? 'bg-success/15' : step.status === 'running' ? 'bg-accent/15' : step.status === 'failed' ? 'bg-red-500/15' : step.status === 'skipped' ? 'bg-amber-500/15' : 'bg-surface-light';

                  return (
                    <motion.div
                      key={step.id}
                      initial={{ opacity: 0, x: -20 }}
                      animate={{ opacity: 1, x: 0 }}
                      transition={{ delay: index * 0.05 }}
                      className={`p-3 rounded-md border transition-all ${isActive ? 'border-accent/50 bg-accent/5' : 'border-border'}`}
                    >
                      <div className="flex items-center gap-3">
                        <div className={`w-7 h-7 rounded-md flex items-center justify-center ${bgColor}`}>
                          <StatusIcon className={`w-3.5 h-3.5 ${statusColor}`} />
                        </div>
                        <div className="flex-1 min-w-0">
                          <div className="flex items-center gap-2">
                            <p className={`text-xs font-medium ${step.status === 'skipped' ? 'text-muted/60' : 'text-foreground'}`}>{step.name}</p>
                            {step.status === 'skipped' && (
                              <span className="text-[9px] font-semibold uppercase tracking-wider text-amber-400 border border-amber-400/40 bg-amber-500/10 rounded px-1.5 py-0.5 leading-none">Skipped</span>
                            )}
                            {step.duration && step.status !== 'skipped' && <span className="text-[10px] text-muted">{step.duration}</span>}
                          </div>
                          <p className="text-[10px] text-muted truncate">{step.description}</p>
                        </div>
                        {step.metrics && (
                          <div className="flex gap-2">
                            {Object.entries(step.metrics).slice(0, 2).map(([key, value]) => (
                              <div key={key} className="text-right">
                                <p className="text-[9px] text-muted capitalize">{key.replace(/([A-Z])/g, ' $1')}</p>
                                <p className="text-[10px] font-medium text-foreground">{typeof value === 'number' ? value.toLocaleString() : value}</p>
                              </div>
                            ))}
                          </div>
                        )}
                      </div>
                      {showDetails && step.details && step.details.length > 0 && (
                        <motion.div
                          initial={{ height: 0, opacity: 0 }}
                          animate={{ height: 'auto', opacity: 1 }}
                          className="mt-2 pt-2 border-t border-border"
                        >
                          <ul className="space-y-0.5">
                            {step.details.map((detail, i) => (
                              <li key={i} className="text-[10px] text-muted flex items-center gap-1.5">
                                <Check className="w-3 h-3 text-success" />
                                {detail}
                              </li>
                            ))}
                          </ul>
                        </motion.div>
                      )}
                    </motion.div>
                  );
                })}
              </div>
            </div>

            {/* Real-time Logs */}
            {showLogs && (
              <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="glass rounded-lg p-4">
                <div className="flex items-center justify-between mb-3">
                  <h4 className="text-xs font-medium text-foreground uppercase tracking-wider flex items-center gap-2">
                    <Terminal className="w-4 h-4" />
                    Real-time Logs
                  </h4>
                  <div className="flex items-center gap-2">
                    <div className="flex items-center gap-1.5 glass-light rounded-md px-2 py-1">
                      <Search className="w-3 h-3 text-muted" />
                      <input
                        type="text"
                        value={logSearch}
                        onChange={(e) => setLogSearch(e.target.value)}
                        placeholder="Search..."
                        className="bg-transparent text-[10px] text-foreground outline-none placeholder-muted w-24"
                      />
                    </div>
                    <div className="flex gap-0.5">
                      {(['all', 'info', 'success', 'warning', 'error'] as const).map(filter => (
                        <button
                          key={filter}
                          onClick={() => setLogFilter(filter)}
                          className={`px-1.5 py-0.5 rounded text-[9px] font-medium transition-all cursor-pointer ${
                            logFilter === filter ? 'bg-accent/15 text-accent-light' : 'text-muted hover:text-foreground hover:bg-surface-light'
                          }`}
                        >
                          {filter.charAt(0).toUpperCase()}
                        </button>
                      ))}
                    </div>
                  </div>
                </div>
                <div className="h-[200px] overflow-y-auto space-y-0.5 pr-2 font-mono text-[10px]">
                  {filteredLogs.map((log) => {
                    const levelColor = log.level === 'success' ? 'text-success' : log.level === 'warning' ? 'text-amber-400' : log.level === 'error' ? 'text-danger' : 'text-muted';
                    const levelBg = log.level === 'success' ? 'bg-success/10' : log.level === 'warning' ? 'bg-amber-500/10' : log.level === 'error' ? 'bg-red-500/10' : 'bg-surface-light';
                    const cat = log.category;
                    const catLabel: Record<string, string> = {
                      preflight: 'PFX', analysis: 'ANL', translate: 'TRN', compile: 'BLD',
                      sanitize: 'SAN', scaffold: 'SKF', validate: 'VAL', repair: 'REP',
                      quality: 'QA', decisions: 'DEC', stage: 'STG', idiom: 'IDM',
                      antipattern: 'APT', property: 'PRP', structural: 'STR',
                      signing: 'SIG', timestamp: 'TSP', accounting: 'CST', 'test-gen': 'TST',
                    };
                    const catTag = cat ? (catLabel[cat] ?? '') : '';
                    return (
                      <div key={log.id} className={`flex gap-2 p-1.5 rounded ${levelBg} items-start`}>
                        <span className="text-muted shrink-0">{log.time}</span>
                        <span className={`uppercase font-bold ${levelColor} px-1.5 min-w-[3.5rem] text-center shrink-0`}>{log.level}</span>
                        {catTag !== '' ? (
                          <span className="shrink-0 text-[8px] font-semibold uppercase tracking-wide px-1 py-px rounded bg-accent/15 text-accent-light border border-accent/25">
                            {catTag}
                          </span>
                        ) : (
                          <span className="w-7 shrink-0" />
                        )}
                        <span className="text-foreground min-w-0 break-words">{sanitizeEngineLogForDisplay(log.message)}</span>
                      </div>
                    );
                  })}
                </div>
              </motion.div>
            )}
          </div>

          {/* Right Column - Metrics & Controls */}
          <div className="space-y-6">
            {/* Quick Actions */}
            <div className="glass rounded-lg p-4">
              <h4 className="text-xs font-medium text-foreground uppercase tracking-wider mb-3 flex items-center gap-2">
                <ArrowRight className="w-4 h-4" />
                Quick Actions
              </h4>
              <div className="space-y-2">
                {currentPhase === 'failed' && (
                  <button
                    onClick={handleRetry}
                    className="w-full py-2.5 rounded-lg gradient-accent text-white text-xs font-semibold hover:opacity-90 transition-opacity flex items-center justify-center gap-2 cursor-pointer"
                  >
                    <Play className="w-3.5 h-3.5" /> Restart Migration
                  </button>
                )}
                {(currentPhase === 'validation' || currentPhase === 'pre-flight' || currentPhase === 'migration' || currentPhase === 'verification') && (
                  <p className="text-[10px] text-muted text-center py-2">
                    Migration in progress... Watch the logs below.
                  </p>
                )}
                <button
                  onClick={() => onNavigate('artifacts', projectId)}
                  className="w-full py-2 rounded-md border border-border text-[10px] text-muted hover:text-foreground hover:bg-surface-light transition-colors flex items-center justify-center gap-2 cursor-pointer"
                >
                  <Eye className="w-3.5 h-3.5" /> View Source Code
                </button>
                <button
                  onClick={() => onNavigate('comparison', projectId)}
                  className="w-full py-2 rounded-md border border-border text-[10px] text-muted hover:text-foreground hover:bg-surface-light transition-colors flex items-center justify-center gap-2 cursor-pointer"
                >
                  <FileCode className="w-3.5 h-3.5" /> View Generated Code
                </button>
                <button
                  onClick={() => onNavigate('settings')}
                  className="w-full py-2 rounded-md border border-border text-[10px] text-muted hover:text-foreground hover:bg-surface-light transition-colors flex items-center justify-center gap-2 cursor-pointer"
                >
                  <Bell className="w-3.5 h-3.5" /> Configure Alerts
                </button>
              </div>
            </div>

            {/* Metrics — live from engine */}
            <div className="glass rounded-lg p-4">
              <h4 className="text-xs font-medium text-foreground uppercase tracking-wider mb-3 flex items-center gap-2">
                <BarChart3 className="w-4 h-4" />
                Live Metrics
              </h4>
              <div className="space-y-3">
                {liveUsage?.accounting?.totals && (currentPhase === 'migration' || currentPhase === 'validation' || currentPhase === 'verification') && (
                  <div className="glass-light rounded-md p-3 border border-accent/20">
                    <div className="flex items-center justify-between">
                      <span className="text-[10px] text-muted">Run cost (live)</span>
                      <span className="text-[10px] font-bold font-mono text-foreground">
                        ${typeof liveUsage.accounting.totals.costUsd === 'number' ? liveUsage.accounting.totals.costUsd.toFixed(4) : '—'}
                      </span>
                    </div>
                    <div className="flex items-center justify-between mt-1">
                      <span className="text-[10px] text-muted">Tokens</span>
                      <span className="text-[10px] font-mono text-foreground">
                        {(liveUsage.accounting.totals.totalTokens ?? 0).toLocaleString()}
                      </span>
                    </div>
                    {liveUsage.redactions && liveUsage.redactions.count > 0 && (
                      <p className="text-[9px] text-muted mt-1">{liveUsage.redactions.count} secret(s) redacted before LLM</p>
                    )}
                  </div>
                )}
                {/* §3.d — live cost bar vs approved P95 budget */}
                {liveCostUsd > 0 && (currentPhase === 'migration' || currentPhase === 'validation' || currentPhase === 'verification') && (() => {
                  const p95 = (() => {
                    const cfg = (project?.config as Record<string, unknown> | null | undefined);
                    const est = cfg?.engineEstimate;
                    if (est && typeof est === 'object') return (est as Record<string, unknown>).p95CostUsd as number | undefined;
                    return undefined;
                  })();
                  const pct = p95 && p95 > 0 ? Math.min(100, (liveCostUsd / p95) * 100) : null;
                  const approaching = p95 != null && liveCostUsd > p95 * 0.9;
                  return (
                    <div className={`glass-light rounded-md p-3 border ${approaching ? 'border-amber-500/40' : 'border-accent/20'}`}>
                      <div className="flex items-center justify-between mb-1.5">
                        <span className="text-[10px] text-muted">Spend vs budget</span>
                        <span className={`text-[10px] font-mono font-bold ${approaching ? 'text-amber-400' : 'text-foreground'}`}>
                          ${liveCostUsd.toFixed(4)}{p95 != null ? ` / $${p95.toFixed(2)}` : ''}
                        </span>
                      </div>
                      {pct != null && (
                        <div className="w-full h-1.5 rounded-full bg-border overflow-hidden">
                          <div
                            className={`h-full rounded-full transition-all ${approaching ? 'bg-amber-400' : 'bg-accent-light'}`}
                            style={{ width: `${pct}%` }}
                          />
                        </div>
                      )}
                      {approaching && (
                        <p className="text-[9px] text-amber-400 mt-1 font-medium">
                          Approaching budget cap — engine will stop at P95.
                        </p>
                      )}
                    </div>
                  );
                })()}
                {/* §3.d — queue position */}
                {queuePosition && currentPhase === 'migration' && (
                  <div className="glass-light rounded-md p-3 border border-border/40">
                    <span className="text-[10px] text-muted">Queue position</span>
                    <span className="text-[10px] font-mono text-foreground ml-2">
                      {queuePosition.position} of {queuePosition.depth}
                    </span>
                  </div>
                )}
                {(() => {
                  type CrShape = {
                    metadata?: {
                      accuracy?: number;
                      linesProcessed?: number;
                      iterations?: number;
                      conversionTime?: number;
                    };
                    quality?: { qualityIndex: number; level?: string; markerApplied?: boolean } | null;
                  } | null;
                  const cr = conversionResult as CrShape;
                  const accuracy = cr?.metadata?.accuracy ?? null;
                  const lines = cr?.metadata?.linesProcessed ?? null;
                  const iterations = cr?.metadata?.iterations ?? null;
                  const time = cr?.metadata?.conversionTime ?? null;
                  const q =
                    cr?.quality && typeof cr.quality === 'object'
                      ? cr.quality
                      : null;
                  const qi = typeof q?.qualityIndex === 'number' && Number.isFinite(q.qualityIndex) ? q.qualityIndex : null;
                  return (
                    <>
                      <div className="glass-light rounded-md p-3 border border-accent/10">
                        <div className="flex items-center justify-between mb-1">
                          <span className="text-[10px] text-muted">Quality index</span>
                          <span className="text-[10px] font-bold text-foreground uppercase tracking-wide">
                            {qi !== null ? `${qi}` : conversionResult ? '—' : '—'}
                          </span>
                        </div>
                        {qi !== null && (
                          <>
                            <div className="h-1.5 bg-surface rounded-full overflow-hidden mb-1.5">
                              <div className="h-full bg-success" style={{ width: `${qi}%` }} />
                            </div>
                            <div className="flex items-center justify-between gap-2">
                              <span className="text-[9px] text-muted">{q?.level != null ? `Band: ${String(q.level)}` : ''}</span>
                              {typeof q?.markerApplied === 'boolean' && (
                                <span
                                  className={`text-[8px] font-semibold px-1.5 py-px rounded uppercase ${
                                    q.markerApplied
                                      ? 'bg-success/15 text-success'
                                      : 'bg-amber-500/15 text-amber-400'
                                  }`}
                                >
                                  {q.markerApplied ? 'AI marker' : 'No marker'}
                                </span>
                              )}
                            </div>
                          </>
                        )}
                        {!qi && conversionResult !== null ? (
                          <p className="text-[9px] text-muted leading-snug mt-1">
                            Metrics unavailable (preflight abort or legacy engine).
                          </p>
                        ) : null}
                      </div>
                      <div className="glass-light rounded-md p-3">
                        <div className="flex items-center justify-between mb-1">
                          <span className="text-[10px] text-muted">Parity score (validator)</span>
                          <span className="text-xs font-bold text-accent-light">{accuracy !== null ? `${accuracy}%` : '—'}</span>
                        </div>
                        <div className="h-1.5 bg-surface rounded-full overflow-hidden">
                          <div className="h-full bg-accent-light" style={{ width: `${accuracy ?? 0}%` }} />
                        </div>
                      </div>
                      <div className="glass-light rounded-md p-3">
                        <p className="text-[10px] text-muted">Lines Processed</p>
                        <p className="text-sm font-bold text-foreground">{lines !== null ? lines.toLocaleString() : '—'}</p>
                      </div>
                      <div className="glass-light rounded-md p-3">
                        <p className="text-[10px] text-muted">Iterations</p>
                        <p className="text-sm font-bold text-foreground">{iterations ?? '—'}</p>
                      </div>
                      <div className="glass-light rounded-md p-3">
                        <p className="text-[10px] text-muted">Conversion Time</p>
                        <p className="text-sm font-bold text-foreground">{time !== null ? `${time.toFixed(1)}s` : '—'}</p>
                      </div>
                      <div className="glass-light rounded-md p-3">
                        <p className="text-[10px] text-muted">Progress</p>
                        <p className="text-sm font-bold text-foreground">{progress.toFixed(0)}%</p>
                      </div>
                    </>
                  );
                })()}
              </div>
            </div>

          </div>
        </div>
      )}

      {/* Completion State */}
      {currentPhase === 'completed' && (() => {
        const crDone = conversionResult as {
          conversionId?: string;
          metadata?: {
            accuracy?: number;
            linesProcessed?: number;
            iterations?: number;
            conversionTime?: number;
            accounting?: {
              totals: { calls: number; promptTokens: number; completionTokens: number; totalTokens: number; costUsd: number; cachedInputTokens?: number; cacheCreationInputTokens?: number };
              byStage: Array<{ stage: string; calls: number; promptTokens: number; completionTokens: number; totalTokens: number; costUsd: number; cachedInputTokens?: number; cacheCreationInputTokens?: number; ensemble?: { candidates: number; selectedScore: number } }>;
            };
            warnings?: string[];
            convergence?: {
              reason: string;
              iterations: number;
              blockingRules?: string[];
            };
          };
          quality?: {
            qualityIndex?: number;
            level?: string;
            markerApplied?: boolean;
            buildReadiness?: { status?: 'compiled' | 'skipped' | 'failed' } | number;
            severityCounts?: { error: number; warning: number; info: number };
            checks?: Array<{
              rule: string;
              title: string;
              applicable: boolean;
              passed: boolean;
              severity?: 'error' | 'warning' | 'info';
              violations: Array<{ file: string; line: number | null; snippet: string }>;
            }>;
            semanticFidelity?: {
              parityScore?: number;
              structuralScore?: number;
              propertyPassRate?: number;
              propertySeverityCounts?: { error?: number; warning?: number; info?: number };
              goldenTestsPassing?: number;
              goldenTestsCount?: number;
            };
            compliance?: {
              decisionLogPresent: boolean;
              signedBundle: boolean;
              replayPassRate?: number | null;
            };
          } | null;
          status?: string;
        } | null;
        const outcomeStatus = String(crDone?.status ?? 'success');
        const outcomePartial = outcomeStatus === 'partial';
        const qDone =
          crDone?.quality != null &&
          typeof crDone.quality === 'object'
            ? crDone.quality
            : null;
        const qiDone =
          qDone &&
          typeof qDone.qualityIndex === 'number' &&
          Number.isFinite(qDone.qualityIndex)
            ? qDone.qualityIndex
            : null;
        const compliance = qDone?.compliance ?? null;
        const redactions = (conversionResult as any)?.redactions as { count: number; byRule?: Record<string, number> } | null ?? null;
        const truncations: Array<{ label: string; original: number; trimmedTo: number }> =
          Array.isArray((conversionResult as any)?.truncations)
            ? (conversionResult as any).truncations
            : [];
        const appliedRules: string[] = (() => {
          const r = (conversionResult as any);
          const arr = r?.metadata?.appliedRules ?? r?.appliedRules ?? r?.decisions?.ruleIds ?? null;
          return Array.isArray(arr) ? arr.filter((x: unknown): x is string => typeof x === 'string') : [];
        })();
        const doneAccounting = crDone?.metadata?.accounting ?? null;
        const doneWarnings = Array.isArray(crDone?.metadata?.warnings) ? crDone!.metadata!.warnings! : [];
        const doneConvergence = crDone?.metadata?.convergence ?? null;
        const qualityChecks = Array.isArray(qDone?.checks) ? qDone!.checks! : [];
        const failingChecks = qualityChecks.filter((c) => c.applicable && !c.passed);
        // top-level severityCounts (new shape) falls back to semanticFidelity.propertySeverityCounts (older shape)
        const topSeverityCounts = qDone?.severityCounts ?? qDone?.semanticFidelity?.propertySeverityCounts ?? null;

        return (
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          className={`glass-light rounded-lg p-6 border ${outcomePartial ? 'border-amber-400/35' : 'border-success/30'}`}
        >
          <div className="flex items-center gap-4 mb-6">
            <div className={`w-16 h-16 rounded-full flex items-center justify-center ${outcomePartial ? 'bg-amber-500/15' : 'bg-success/15'}`}>
              <CheckCircle2 className={`w-8 h-8 ${outcomePartial ? 'text-amber-400' : 'text-success'}`} />
            </div>
            <div>
              <h3 className="text-lg font-semibold text-foreground">
                {outcomePartial ? 'Migration Completed with Warnings' : 'Migration Completed Successfully'}
              </h3>
              <p className="text-sm text-muted">
                {conversionResult
                  ? [
                      qiDone != null ? `Quality index ${qiDone}${qDone?.level ? ` (${qDone.level})` : ''}` : null,
                      `Parity (validator): ${crDone?.metadata?.accuracy ?? '—'}%`,
                      `${(crDone?.metadata?.linesProcessed ?? 0).toLocaleString()} lines processed`,
                      typeof qDone?.markerApplied === 'boolean'
                        ? qDone.markerApplied
                          ? '@scriba-ai-generated marker applied.'
                          : 'AI marker not applied — output stayed below marker threshold.'
                        : null,
                    ]
                      .filter(Boolean)
                      .join(' · ')
                  : 'All quality gates passed. Project is ready for deployment.'}
              </p>
            </div>
          </div>

          {conversionResult !== null ? (
            <div className="glass-light rounded-lg p-4 mb-6 border border-border">
              <p className="text-[10px] text-muted uppercase tracking-wider mb-2 flex items-center gap-2">
                <Medal className="w-3.5 h-3.5" /> Quality summary
              </p>
              {qiDone !== null ? (
                <div className="flex flex-wrap items-center gap-3">
                  <div className="flex-1 min-w-[12rem]">
                    <div className="flex items-end justify-between gap-2 mb-1">
                      <span className="text-3xl font-bold text-foreground tabular-nums">{qiDone}</span>
                      <span className="text-muted text-sm">/ 100</span>
                      {qDone?.level != null ? (
                        <span className="ml-auto text-xs font-semibold px-2 py-0.5 rounded-md bg-accent/15 text-accent-light border border-accent/25 uppercase">
                          {String(qDone.level)}
                        </span>
                      ) : null}
                    </div>
                    <div className="h-2 bg-surface rounded-full overflow-hidden">
                      <div className="h-full bg-success rounded-full transition-all" style={{ width: `${qiDone}%` }} />
                    </div>
                  </div>
                  {typeof qDone?.markerApplied === 'boolean' ? (
                    <div
                      className={`text-[11px] rounded-md px-3 py-2 max-w-xs leading-snug ${
                        qDone.markerApplied ? 'bg-success/10 text-success' : 'bg-amber-500/10 text-amber-400'
                      }`}
                    >
                      {qDone.markerApplied
                        ? 'Output is tagged for downstream compliance as meeting the minimum AI fitness threshold.'
                        : 'The engine did not apply the marker because the score stayed below threshold; review before treating this as certified output.'}
                    </div>
                  ) : null}
                </div>
              ) : (
                <p className="text-sm text-muted">Metrics unavailable — this run stopped before aggregate quality was computed.</p>
              )}
            </div>
          ) : null}
          
          {/* ML01 quality axes */}
          {(() => {
            const sf = qDone?.semanticFidelity;
            const br = qDone?.buildReadiness;
            const brStatus = typeof br === 'object' && br != null ? br.status : undefined;
            const severityCounts = topSeverityCounts;
            const propertyRate = typeof sf?.propertyPassRate === 'number' ? sf.propertyPassRate : null;
            const goldenPassing = typeof sf?.goldenTestsPassing === 'number' ? sf.goldenTestsPassing : null;
            const goldenCount = typeof sf?.goldenTestsCount === 'number' ? sf.goldenTestsCount : null;
            const structScore = typeof sf?.structuralScore === 'number' ? sf.structuralScore : null;
            const hasAnyAxes = brStatus || severityCounts !== null || propertyRate !== null || goldenPassing !== null || structScore !== null;
            if (!hasAnyAxes) return null;
            // propertySeverityCounts badge helpers (ML01 §5.2 — recommended primary signal)
            const scErr = (severityCounts?.error ?? 0);
            const scWarn = (severityCounts?.warning ?? 0);
            const scInfo = (severityCounts?.info ?? 0);
            const scTotal = scErr + scWarn + scInfo;
            const scLabel = scErr > 0
              ? `${scErr} convergence-blocking issue${scErr !== 1 ? 's' : ''}`
              : scWarn > 0
                ? `${scWarn} warning${scWarn !== 1 ? 's' : ''}`
                : scInfo > 0
                  ? `${scInfo} info`
                  : 'Properties clean';
            const scClass = scErr > 0
              ? 'bg-red-500/10 border-red-500/30 text-red-400'
              : scWarn > 0
                ? 'bg-amber-500/10 border-amber-400/30 text-amber-400'
                : scTotal > 0
                  ? 'bg-surface border-border text-muted'
                  : 'bg-success/10 border-success/30 text-success';
            return (
              <div className="glass-light rounded-lg p-4 mb-4 border border-border">
                <p className="text-[10px] text-muted uppercase tracking-wider mb-3 flex items-center gap-2">
                  <Target className="w-3.5 h-3.5" /> Quality axes
                </p>
                <div className="flex flex-wrap gap-3">
                  {brStatus && (
                    <div className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md border ${
                      brStatus === 'compiled' ? 'bg-success/10 border-success/30 text-success'
                      : brStatus === 'skipped' ? 'bg-amber-500/10 border-amber-400/30 text-amber-400'
                      : 'bg-red-500/10 border-red-500/30 text-red-400'
                    }`}>
                      {brStatus === 'compiled' ? <CheckCircle2 className="w-3.5 h-3.5" /> : <AlertTriangle className="w-3.5 h-3.5" />}
                      {brStatus === 'compiled' ? 'Build compiled' : brStatus === 'skipped' ? 'Build unverified (toolchain not on PATH)' : 'Build failed'}
                    </div>
                  )}
                  {/* propertySeverityCounts is the primary signal; propertyPassRate is the fallback */}
                  {severityCounts !== null ? (
                    <div className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md border ${scClass}`}>
                      <Shield className="w-3.5 h-3.5" />
                      {scLabel}
                    </div>
                  ) : propertyRate !== null ? (
                    <div className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md border ${
                      propertyRate >= 0.8 ? 'bg-success/10 border-success/30 text-success'
                      : propertyRate >= 0.6 ? 'bg-amber-500/10 border-amber-400/30 text-amber-400'
                      : 'bg-red-500/10 border-red-500/30 text-red-400'
                    }`}>
                      <Shield className="w-3.5 h-3.5" />
                      Property checks: {(propertyRate * 100).toFixed(0)}% pass
                    </div>
                  ) : null}
                  {goldenPassing !== null ? (
                    <div className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md border ${
                      goldenCount != null && goldenPassing >= goldenCount ? 'bg-success/10 border-success/30 text-success'
                      : goldenPassing > 0 ? 'bg-amber-500/10 border-amber-400/30 text-amber-400'
                      : 'bg-red-500/10 border-red-500/30 text-red-400'
                    }`}>
                      <CheckCircle2 className="w-3.5 h-3.5" />
                      Tests: {goldenPassing}{goldenCount != null ? ` / ${goldenCount}` : ''} passing
                    </div>
                  ) : (
                    <div className="flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md border border-border text-muted">
                      <Clock className="w-3.5 h-3.5" />
                      Tests not run
                    </div>
                  )}
                  {structScore !== null && (
                    <div className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md border ${
                      structScore >= 90 ? 'bg-success/10 border-success/30 text-success'
                      : structScore >= 70 ? 'bg-amber-500/10 border-amber-400/30 text-amber-400'
                      : 'bg-red-500/10 border-red-500/30 text-red-400'
                    }`}>
                      <Layers className="w-3.5 h-3.5" />
                      Structural match: {structScore.toFixed(0)}%
                    </div>
                  )}
                </div>
              </div>
            );
          })()}

          {/* Convergence reason + blocking rules (§3) */}
          {doneConvergence && (doneConvergence.reason !== 'converged' || (doneConvergence.blockingRules && doneConvergence.blockingRules.length > 0)) && (() => {
            const isPropertyError = doneConvergence.reason === 'property-error';
            const blocking = doneConvergence.blockingRules ?? [];
            const blockingWithTitles = blocking.map((rid) => {
              const match = qualityChecks.find((c) => c.rule === rid);
              return match ? match.title : rid;
            });
            return (
              <div className={`glass-light rounded-lg p-4 mb-4 border ${isPropertyError ? 'border-red-500/30' : 'border-amber-400/30'}`}>
                <p className={`text-[10px] uppercase tracking-wider mb-2 flex items-center gap-2 ${isPropertyError ? 'text-red-400' : 'text-amber-400'}`}>
                  <AlertTriangle className="w-3.5 h-3.5" />
                  {isPropertyError ? 'Stopped: convergence-blocking issues' : `Stopped: ${doneConvergence.reason.replace(/-/g, ' ')}`}
                </p>
                {isPropertyError && blockingWithTitles.length > 0 && (
                  <>
                    <p className="text-xs text-muted mb-2">These hard-error rules were still failing on the last iteration:</p>
                    <ul className="text-xs text-red-300 space-y-0.5 list-disc pl-4">
                      {blockingWithTitles.map((t, i) => <li key={i}>{t}</li>)}
                    </ul>
                  </>
                )}
                <p className="text-[10px] text-muted mt-2">Iterations used: {doneConvergence.iterations}</p>
              </div>
            );
          })()}

          {/* Property checks drilldown — failing checks grouped by severity (§1.2, §2.2) */}
          {failingChecks.length > 0 && (() => {
            const bySev = {
              error: failingChecks.filter((c) => (c.severity ?? 'warning') === 'error'),
              warning: failingChecks.filter((c) => (c.severity ?? 'warning') === 'warning'),
              info: failingChecks.filter((c) => (c.severity ?? 'warning') === 'info'),
            };
            const groups = ([
              { sev: 'error' as const, label: 'Errors', cls: 'text-red-400', items: bySev.error },
              { sev: 'warning' as const, label: 'Warnings', cls: 'text-amber-400', items: bySev.warning },
              { sev: 'info' as const, label: 'Info', cls: 'text-muted', items: bySev.info },
            ] as const).filter((g) => g.items.length > 0);
            return (
              <div className="glass-light rounded-lg p-4 mb-4 border border-border">
                <p className="text-[10px] text-muted uppercase tracking-wider mb-3 flex items-center gap-2">
                  <Shield className="w-3.5 h-3.5" /> Property rule violations
                </p>
                <div className="space-y-4">
                  {groups.map(({ sev, label, cls, items }) => (
                    <div key={sev}>
                      <p className={`text-[10px] font-semibold uppercase tracking-wider mb-1.5 ${cls}`}>{label}</p>
                      <div className="space-y-2">
                        {items.map((c) => (
                          <div key={c.rule} className="rounded-md bg-surface p-2.5">
                            <p className="text-xs font-medium text-foreground mb-1">{c.title}</p>
                            {c.violations.length > 0 && (
                              <ul className="space-y-0.5">
                                {c.violations.slice(0, 5).map((v, i) => (
                                  <li key={i} className="text-[11px] text-muted font-mono">
                                    {v.file}{v.line != null ? `:${v.line}` : ''}{v.snippet ? ` — ${v.snippet}` : ''}
                                  </li>
                                ))}
                                {c.violations.length > 5 && (
                                  <li className="text-[11px] text-muted">+{c.violations.length - 5} more</li>
                                )}
                              </ul>
                            )}
                          </div>
                        ))}
                      </div>
                    </div>
                  ))}
                </div>
              </div>
            );
          })()}

          {/* Success Metrics Grid */}
          <div className="grid grid-cols-4 gap-4 mb-6">
            <div className="glass-light rounded-md p-3 text-center">
              <p className="text-[10px] text-muted uppercase tracking-wider mb-1">Parity (validator)</p>
              <p className="text-2xl font-bold text-success">{(conversionResult as any)?.metadata?.accuracy ?? '—'}%</p>
            </div>
            <div className="glass-light rounded-md p-3 text-center">
              <p className="text-[10px] text-muted uppercase tracking-wider mb-1">Lines Processed</p>
              <p className="text-2xl font-bold text-accent-light">{((conversionResult as any)?.metadata?.linesProcessed ?? 0).toLocaleString()}</p>
            </div>
            <div className="glass-light rounded-md p-3 text-center">
              <p className="text-[10px] text-muted uppercase tracking-wider mb-1">Iterations</p>
              <p className="text-2xl font-bold text-purple-400">{(conversionResult as any)?.metadata?.iterations ?? '—'}</p>
            </div>
            <div className="glass-light rounded-md p-3 text-center">
              <p className="text-[10px] text-muted uppercase tracking-wider mb-1">Time</p>
              <p className="text-2xl font-bold text-amber-400">{(conversionResult as any)?.metadata?.conversionTime ? `${((conversionResult as any).metadata.conversionTime).toFixed(1)}s` : '—'}</p>
            </div>
          </div>

          {/* Skipped modules notice */}
          {steps.some(s => s.status === 'skipped') && (
            <div className="glass-light rounded-lg p-4 mb-6 border border-amber-400/25">
              <p className="text-[10px] text-amber-400 uppercase tracking-wider mb-2 flex items-center gap-2">
                <AlertTriangle className="w-3.5 h-3.5" /> Stages not executed
              </p>
              <p className="text-xs text-muted mb-2">
                The following pipeline modules were disabled for this run and are not reflected in the report:
              </p>
              <div className="flex flex-wrap gap-1.5">
                {steps.filter(s => s.status === 'skipped').map(s => (
                  <span key={s.id} className="text-[10px] font-medium text-amber-400 bg-amber-500/10 border border-amber-400/30 rounded px-2 py-0.5">
                    {s.name}
                  </span>
                ))}
              </div>
            </div>
          )}

          {/* Dependency mapping panel */}
          {(() => {
            const completionCfg = parseProjectConfig(project);
            const dm = Array.isArray(completionCfg.dependencyMapping) ? completionCfg.dependencyMapping as Array<Record<string, unknown>> : null;
            if (!dm || dm.length === 0) return null;
            const autoCnt = dm.filter(e => e.status === 'auto').length;
            const modifiedCnt = dm.filter(e => e.status === 'modified').length;
            const addedCnt = dm.filter(e => e.status === 'added').length;
            const rejectedCnt = dm.filter(e => e.status === 'removed').length;
            const activeDm = dm.filter(e => e.status !== 'removed');
            return (
              <div className="glass-light rounded-lg p-4 mb-6 border border-border">
                <p className="text-[10px] text-muted uppercase tracking-wider mb-3 flex items-center gap-2">
                  <Network className="w-3.5 h-3.5" /> Dependency Mapping ({activeDm.length} active{rejectedCnt > 0 ? `, ${rejectedCnt} rejected` : ''})
                </p>
                <div className="flex gap-2 mb-3">
                  {[
                    { label: 'Auto', count: autoCnt, cls: 'text-blue-400 border-blue-400/30 bg-blue-500/10' },
                    { label: 'Modified', count: modifiedCnt, cls: 'text-amber-400 border-amber-400/30 bg-amber-500/10' },
                    { label: 'Added', count: addedCnt, cls: 'text-green-400 border-green-400/30 bg-green-500/10' },
                    { label: 'Rejected', count: rejectedCnt, cls: 'text-red-400 border-red-400/30 bg-red-500/10' },
                  ].filter(s => s.count > 0).map(s => (
                    <span key={s.label} className={`text-[10px] font-semibold px-2 py-0.5 rounded border leading-none ${s.cls}`}>
                      {s.count} {s.label}
                    </span>
                  ))}
                </div>
                <div className="space-y-1 max-h-40 overflow-y-auto">
                  {activeDm.map((e, i) => (
                    <div key={i} className="flex items-center gap-2 text-xs font-mono">
                      <span className="text-muted w-28 truncate">{String(e.source ?? '')}</span>
                      <span className="text-muted/50">→</span>
                      <span className="text-foreground flex-1 truncate">{String(e.target ?? '')}</span>
                      {e.version != null && <span className="text-muted/70 text-[10px]">{String(e.version)}</span>}
                    </div>
                  ))}
                </div>
              </div>
            );
          })()}

          {/* §5.3 — Compliance evidence panel */}
          {compliance !== null && (
            <div className="glass-light rounded-lg p-4 mb-6 border border-border">
              <p className="text-[10px] text-muted uppercase tracking-wider mb-3 flex items-center gap-2">
                <Shield className="w-3.5 h-3.5" /> Compliance evidence
              </p>
              <ul className="space-y-2 text-xs mb-4">
                <li className="flex items-start gap-2">
                  <span className={compliance.decisionLogPresent ? 'text-success' : 'text-amber-400'}>
                    {compliance.decisionLogPresent ? '✅' : '⚠️'}
                  </span>
                  <span className={`flex-1 flex items-center gap-2 ${compliance.decisionLogPresent ? 'text-foreground' : 'text-amber-400'}`}>
                    {compliance.decisionLogPresent
                      ? 'Decision log present (MIGRATION-DECISIONS.md)'
                      : 'Decision log not emitted (no decisions recorded)'}
                    {compliance.decisionLogPresent && (
                      <button
                        onClick={handleDownloadDecisionLog}
                        className="ml-1 flex items-center gap-1 text-[10px] text-accent-light hover:underline cursor-pointer"
                        title="Download MIGRATION-DECISIONS.md"
                      >
                        <Download className="w-3 h-3" /> Download
                      </button>
                    )}
                  </span>
                </li>
                {appliedRules.length > 0 && (() => {
                  const CAT_ORDER: RuleCategory[] = ['kg', 'pair', 'target-language', 'universal', 'other'];
                  const CAT_LABEL: Record<RuleCategory, string> = {
                    kg: 'KG cross-reference',
                    pair: 'Pair-specific',
                    'target-language': 'Target language',
                    universal: 'Universal',
                    other: 'Other',
                  };
                  const grouped = CAT_ORDER.map(cat => ({
                    cat,
                    ids: appliedRules.filter(id => categoryOf(id) === cat),
                  })).filter(g => g.ids.length > 0);
                  return (
                    <li className="mt-2">
                      <p className="text-[10px] text-muted uppercase tracking-wider mb-2">Applied rules</p>
                      <div className="space-y-2">
                        {grouped.map(({ cat, ids }) => (
                          <div key={cat}>
                            <p className="text-[9px] text-muted uppercase tracking-widest mb-1">{CAT_LABEL[cat]}</p>
                            <div className="flex flex-wrap gap-1.5">
                              {ids.map(id => {
                                const rl = getRuleLabel(id);
                                const cls = getRuleSeverityClass(id);
                                return (
                                  <span
                                    key={id}
                                    title={rl.tooltip}
                                    className={`text-[10px] font-medium px-2 py-0.5 rounded border border-border bg-surface-light cursor-help ${cls}`}
                                  >
                                    {rl.label}
                                  </span>
                                );
                              })}
                            </div>
                          </div>
                        ))}
                      </div>
                    </li>
                  );
                })()}
                <li className="flex items-start gap-2">
                  <span className={compliance.signedBundle ? 'text-success' : 'text-amber-400'}>
                    {compliance.signedBundle ? '✅' : '⚠️'}
                  </span>
                  <span className={compliance.signedBundle ? 'text-foreground' : 'text-amber-400'}>
                    {compliance.signedBundle
                      ? 'Bundle signed (Ed25519 JWS)'
                      : 'Signing not configured on engine — bundle is not legally defensible'}
                  </span>
                </li>
                <li className="flex items-start gap-2">
                  <span className={
                    compliance.replayPassRate == null ? 'text-muted'
                    : compliance.replayPassRate >= 0.95 ? 'text-success'
                    : 'text-amber-400'
                  }>
                    {compliance.replayPassRate == null ? '⏳' : compliance.replayPassRate >= 0.95 ? '✅' : '⚠️'}
                  </span>
                  <span className={
                    compliance.replayPassRate == null ? 'text-muted'
                    : compliance.replayPassRate >= 0.95 ? 'text-foreground'
                    : 'text-amber-400'
                  }>
                    {compliance.replayPassRate == null
                      ? 'Replay pass rate: not measured (pilot)'
                      : compliance.replayPassRate >= 0.95
                        ? `${(compliance.replayPassRate * 100).toFixed(0)}% replay pass rate`
                        : `Replay below WOW threshold (${(compliance.replayPassRate * 100).toFixed(0)}%)`}
                  </span>
                </li>
              </ul>
              <div className="flex gap-2">
                <a
                  href="#"
                  onClick={(e) => { e.preventDefault(); onNavigate('artifacts', projectId); }}
                  className="text-[10px] px-3 py-1.5 rounded-md border border-border hover:bg-surface-light transition-colors flex items-center gap-1.5 cursor-pointer"
                >
                  <Download className="w-3 h-3" /> Download for auditor
                </a>
                <a
                  href="#"
                  onClick={(e) => { e.preventDefault(); onNavigate('artifacts', projectId); }}
                  className="text-[10px] px-3 py-1.5 rounded-md border border-border hover:bg-surface-light transition-colors flex items-center gap-1.5 cursor-pointer"
                >
                  <ExternalLink className="w-3 h-3" /> scriba-verify ↓
                </a>
              </div>
            </div>
          )}

          {/* §5.3a — Decision journal inline viewer */}
          {compliance?.decisionLogPresent && (
            <div className="glass-light rounded-lg mb-6 border border-border overflow-hidden">
              <button
                onClick={() => {
                  setDecisionJournalOpen(prev => !prev);
                  if (!decisionJournalOpen && decisionJournalContent === null) {
                    setDecisionJournalLoading(true);
                    const filePaths = ((conversionResult as { files?: Array<{ path?: string }> })?.files ?? [])
                      .map((f: { path?: string }) => f?.path)
                      .filter((p: unknown): p is string => typeof p === 'string' && p.length > 0);
                    const { aggregateCandidates } = buildMigrationDecisionPaths(filePaths);
                    (async () => {
                      for (const candidate of aggregateCandidates) {
                        try {
                          const res = await fetch(`/api/source-file?path=${encodeURIComponent(candidate)}`);
                          if (!res.ok) continue;
                          const data = await res.json() as { content?: string };
                          if (!data.content) continue;
                          setDecisionJournalContent(data.content);
                          setDecisionJournalLoading(false);
                          return;
                        } catch { continue; }
                      }
                      setDecisionJournalContent('');
                      setDecisionJournalLoading(false);
                    })();
                  }
                }}
                className="w-full flex items-center justify-between px-4 py-3 text-left hover:bg-surface-light/50 transition-colors cursor-pointer"
              >
                <span className="text-[10px] text-muted uppercase tracking-wider flex items-center gap-2">
                  <FileText className="w-3.5 h-3.5" /> Decision Journal (MIGRATION-DECISIONS.md)
                </span>
                <ChevronRight className={`w-3.5 h-3.5 text-muted transition-transform ${decisionJournalOpen ? 'rotate-90' : ''}`} />
              </button>
              {decisionJournalOpen && (
                <div className="border-t border-border px-4 py-3 max-h-96 overflow-y-auto">
                  {decisionJournalLoading && (
                    <div className="flex items-center gap-2 text-xs text-muted">
                      <Loader2 className="w-3.5 h-3.5 animate-spin" /> Loading decision journal…
                    </div>
                  )}
                  {!decisionJournalLoading && decisionJournalContent === '' && (
                    <p className="text-xs text-muted">Decision journal not found in output bundle.</p>
                  )}
                  {!decisionJournalLoading && decisionJournalContent && (
                    <pre className="text-[10px] font-mono text-muted whitespace-pre-wrap leading-relaxed">
                      {decisionJournalContent}
                    </pre>
                  )}
                </div>
              )}
            </div>
          )}

          {/* §5.3 — Warnings list (level downgrade notices, etc.) */}
          {doneWarnings.length > 0 && (
            <div className="glass-light rounded-lg p-4 mb-6 border border-amber-400/30">
              <p className="text-[10px] text-amber-400 uppercase tracking-wider mb-2 flex items-center gap-2">
                <AlertTriangle className="w-3.5 h-3.5" /> Warnings
              </p>
              <ul className="text-xs text-muted space-y-1 list-disc pl-4">
                {doneWarnings.map((w, i) => <li key={i}>{w}</li>)}
              </ul>
            </div>
          )}

          {/* §5.3b — Redactions panel (compliance evidence) */}
          {redactions && redactions.count > 0 && (
            <div className="glass-light rounded-lg p-4 mb-6 border border-border">
              <p className="text-[10px] text-muted uppercase tracking-wider mb-2 flex items-center gap-2">
                <Shield className="w-3.5 h-3.5" /> Privacy — Redactions Applied
              </p>
              <p className="text-xs text-foreground">
                <span className="font-semibold">{redactions.count}</span> secret{redactions.count !== 1 ? 's' : ''} / PII item{redactions.count !== 1 ? 's' : ''} were scrubbed from source before AI processing.
              </p>
              {redactions.byRule && Object.keys(redactions.byRule).length > 0 && (
                <div className="flex flex-wrap gap-1.5 mt-2">
                  {Object.entries(redactions.byRule).map(([rule, cnt]) => (
                    <span key={rule} className="text-[10px] px-2 py-0.5 rounded bg-surface border border-border text-muted">
                      {rule}: {cnt}
                    </span>
                  ))}
                </div>
              )}
            </div>
          )}

          {/* §5.3c — Truncations panel */}
          {truncations.length > 0 && (
            <div className="glass-light rounded-lg p-4 mb-6 border border-amber-400/30">
              <p className="text-[10px] text-amber-400 uppercase tracking-wider mb-2 flex items-center gap-2">
                <AlertTriangle className="w-3.5 h-3.5" /> File Truncations
              </p>
              <p className="text-[10px] text-muted mb-2">
                {truncations.length} file{truncations.length !== 1 ? 's' : ''} exceeded the prompt size limit and were trimmed before processing. Translation may be incomplete for these files.
              </p>
              <ul className="space-y-1">
                {truncations.map((t, i) => (
                  <li key={i} className="text-xs text-foreground flex items-center gap-2">
                    <span className="font-mono text-muted truncate flex-1">{t.label}</span>
                    <span className="text-[10px] text-muted shrink-0">
                      {t.original.toLocaleString()} → {t.trimmedTo.toLocaleString()} tokens
                    </span>
                  </li>
                ))}
              </ul>
            </div>
          )}

          {/* §5.4 — Cost breakdown (full table) */}
          {doneAccounting !== null && isAccountingSnapshot(doneAccounting) ? (() => {
            const promptTokens = doneAccounting.totals.promptTokens ?? 0;
            const cachedTokens = doneAccounting.totals.cachedInputTokens ?? 0;
            const savedRatio = promptTokens > 0 ? cachedTokens / promptTokens : 0;
            const cacheColor = savedRatio >= 0.3 ? 'text-success' : savedRatio >= 0.1 ? 'text-amber-400' : 'text-muted';
            const hasEnsemble = doneAccounting.byStage.some((s) => s.ensemble != null);
            const hasAnyCacheData = doneAccounting.byStage.some(
              (s) => (s.cachedInputTokens ?? 0) > 0 || (s.cacheCreationInputTokens ?? 0) > 0
            );
            return (
            <div className="glass-light rounded-lg p-4 mb-6 border border-border">
              <div className="flex items-center justify-between mb-3">
                <p className="text-[10px] text-muted uppercase tracking-wider flex items-center gap-2">
                  <BarChart3 className="w-3.5 h-3.5" /> Cost breakdown
                </p>
                <div className="flex items-center gap-3">
                  {cachedTokens > 0 && (
                    <span className={`text-[10px] ${cacheColor}`}>
                      Prompt-cache hit rate: {(savedRatio * 100).toFixed(0)}%
                    </span>
                  )}
                  <span className="text-sm font-bold text-foreground font-mono">
                    ${typeof doneAccounting.totals.costUsd === 'number' ? doneAccounting.totals.costUsd.toFixed(4) : '—'}
                  </span>
                </div>
              </div>
              <div className="overflow-x-auto">
                <table className="w-full text-xs text-left">
                  <thead>
                    <tr className="text-muted border-b border-border">
                      <th className="pb-1.5 pr-3 font-medium">Stage</th>
                      <th className="pb-1.5 pr-3 font-medium text-right">Calls</th>
                      <th className="pb-1.5 pr-3 font-medium text-right">Tokens</th>
                      {hasAnyCacheData && <th className="pb-1.5 pr-3 font-medium text-right text-success" title="Prompt-cache read hits">Cache hit</th>}
                      {hasAnyCacheData && <th className="pb-1.5 pr-3 font-medium text-right text-amber-400" title="Tokens written to prompt cache">Cache write</th>}
                      <th className="pb-1.5 font-medium text-right">USD</th>
                    </tr>
                  </thead>
                  <tbody className="divide-y divide-border">
                    {doneAccounting.byStage.map((s) => (
                      <tr key={s.stage} className="text-foreground">
                        <td className="py-1.5 pr-3 text-muted">
                          <span>{labelAccountingStage(s.stage)}</span>
                          {s.ensemble != null && (
                            <span className="ml-1.5 text-[9px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded bg-purple-500/15 text-purple-400 border border-purple-500/25">
                              Ensemble ×{s.ensemble.candidates}
                            </span>
                          )}
                          {hasEnsemble && s.ensemble == null && <span />}
                        </td>
                        <td className="py-1.5 pr-3 text-right tabular-nums">{s.calls}</td>
                        <td className="py-1.5 pr-3 text-right tabular-nums">{(s.totalTokens ?? 0).toLocaleString()}</td>
                        {hasAnyCacheData && (
                          <td className="py-1.5 pr-3 text-right tabular-nums text-success">
                            {(s.cachedInputTokens ?? 0) > 0 ? (s.cachedInputTokens!).toLocaleString() : '—'}
                          </td>
                        )}
                        {hasAnyCacheData && (
                          <td className="py-1.5 pr-3 text-right tabular-nums text-amber-400">
                            {(s.cacheCreationInputTokens ?? 0) > 0 ? (s.cacheCreationInputTokens!).toLocaleString() : '—'}
                          </td>
                        )}
                        <td className="py-1.5 text-right tabular-nums font-mono">${typeof s.costUsd === 'number' ? s.costUsd.toFixed(4) : '—'}</td>
                      </tr>
                    ))}
                  </tbody>
                  <tfoot>
                    <tr className="border-t border-border font-semibold text-foreground">
                      <td className="pt-2 pr-3">Total</td>
                      <td className="pt-2 pr-3 text-right tabular-nums">{doneAccounting.totals.calls}</td>
                      <td className="pt-2 pr-3 text-right tabular-nums">{(doneAccounting.totals.totalTokens ?? 0).toLocaleString()}</td>
                      {hasAnyCacheData && (
                        <td className="pt-2 pr-3 text-right tabular-nums text-success">
                          {(doneAccounting.totals.cachedInputTokens ?? 0).toLocaleString()}
                        </td>
                      )}
                      {hasAnyCacheData && (
                        <td className="pt-2 pr-3 text-right tabular-nums text-amber-400">
                          {(doneAccounting.totals.cacheCreationInputTokens ?? 0).toLocaleString()}
                        </td>
                      )}
                      <td className="pt-2 text-right tabular-nums font-mono">${typeof doneAccounting.totals.costUsd === 'number' ? doneAccounting.totals.costUsd.toFixed(4) : '—'}</td>
                    </tr>
                  </tfoot>
                </table>
              </div>
            </div>
            );
          })() : null}

          {/* ML01 §8 — HITL: approved files panel (uses /runs/:id/approvals + /approvals/diff) */}
          {(activeRunId || crDone?.conversionId) && (hilApprovedFiles.length > 0 || hilPendingPaths.length > 0) && (
            <div className="glass-light rounded-lg p-4 mb-6 border border-border">
              <div className="flex items-center justify-between mb-3">
                <p className="text-[10px] text-muted uppercase tracking-wider flex items-center gap-2">
                  <Check className="w-3.5 h-3.5" /> Human-in-the-loop approvals
                </p>
                <span className="text-[10px] text-muted">
                  {hilApprovedFiles.length} / {Math.max(hilApprovedFiles.length, hilPendingPaths.length)} approved
                </span>
              </div>
              {(() => {
                const runIdForHitl = activeRunId;
                const allPaths = hilPendingPaths.length > 0
                  ? hilPendingPaths
                  : hilApprovedFiles;
                if (allPaths.length === 0) {
                  return <p className="text-xs text-muted">No files pending review.</p>;
                }
                return (
                  <ul className="space-y-1">
                    {allPaths.map((p) => {
                      const isApproved = hilApprovedFiles.includes(p);
                      const leaf = p.split('/').pop() ?? p;
                      return (
                        <li key={p} className="flex items-center gap-2 text-xs">
                          <span className={`text-[9px] font-semibold px-1.5 py-px rounded shrink-0 ${
                            isApproved ? 'bg-success/15 text-success' : 'bg-amber-500/15 text-amber-400'
                          }`}>
                            {isApproved ? 'Approved' : 'Pending'}
                          </span>
                          <span className="text-foreground font-mono truncate flex-1">{leaf}</span>
                          {runIdForHitl && (
                            <>
                              <button
                                onClick={async () => {
                                  if (isApproved) {
                                    await api.engine.submitApproval(runIdForHitl, p, 'reject').catch(() => {});
                                    setHilApprovedFiles((prev) => prev.filter((x) => x !== p));
                                  } else {
                                    await api.engine.submitApproval(runIdForHitl, p, 'approve').catch(() => {});
                                    setHilApprovedFiles((prev) => [...prev, p]);
                                  }
                                }}
                                className="shrink-0 text-[9px] px-2 py-px rounded border border-border hover:bg-surface-light transition-colors cursor-pointer"
                              >
                                {isApproved ? 'Reject' : 'Approve'}
                              </button>
                              <button
                                onClick={async () => {
                                  if (hilDiffFile === p) { setHilDiffFile(null); setHilDiff(null); return; }
                                  const diff = await api.engine.getApprovalDiff(runIdForHitl, p).catch(() => null);
                                  setHilDiffFile(p);
                                  setHilDiff(diff ? { source: diff.source, target: diff.target } : null);
                                }}
                                className="shrink-0 text-[9px] px-2 py-px rounded border border-border hover:bg-surface-light transition-colors cursor-pointer flex items-center gap-1"
                              >
                                <GitCompare className="w-2.5 h-2.5" /> Diff
                              </button>
                            </>
                          )}
                        </li>
                      );
                    })}
                  </ul>
                );
              })()}
              {hilDiffFile && hilDiff !== null && (
                <div className="mt-3 border-t border-border pt-3">
                  <p className="text-[10px] text-muted mb-2 font-mono">{hilDiffFile.split('/').pop()} — source vs. target</p>
                  <div className="grid grid-cols-2 gap-2 max-h-48 overflow-y-auto">
                    <div>
                      <p className="text-[9px] text-muted mb-1">Source</p>
                      <pre className="text-[9px] font-mono text-muted bg-surface rounded p-2 overflow-x-auto whitespace-pre-wrap">{hilDiff.source}</pre>
                    </div>
                    <div>
                      <p className="text-[9px] text-muted mb-1">Target</p>
                      <pre className="text-[9px] font-mono text-foreground bg-surface rounded p-2 overflow-x-auto whitespace-pre-wrap">{hilDiff.target}</pre>
                    </div>
                  </div>
                </div>
              )}
              {hilDiffFile && hilDiff === null && (
                <p className="mt-2 text-xs text-muted">No diff available for this file.</p>
              )}
            </div>
          )}

          {/* Docker Sandbox Panel */}
          {sandboxPhase !== 'idle' && (
            <motion.div
              initial={{ opacity: 0, y: 16 }}
              animate={{ opacity: 1, y: 0 }}
              className={`glass-light rounded-lg border mb-6 overflow-hidden ${
                sandboxPhase === 'success' ? 'border-success/30'
                : sandboxPhase === 'failed' ? 'border-red-500/30'
                : 'border-accent/30'
              }`}
            >
              {/* Header */}
              <div className="flex items-center justify-between px-4 py-3 border-b border-border">
                <div className="flex items-center gap-2">
                  <Container className={`w-4 h-4 ${sandboxPhase === 'success' ? 'text-success' : sandboxPhase === 'failed' ? 'text-danger' : 'text-accent-light'}`} />
                  <h4 className="text-xs font-semibold text-foreground uppercase tracking-wider">Sandbox Build Verification</h4>
                  {sandboxAttempt > 1 && (
                    <span className="text-[9px] font-semibold px-1.5 py-px rounded bg-amber-500/15 text-amber-400 border border-amber-400/30">
                      Repair attempt {sandboxAttempt}/3
                    </span>
                  )}
                </div>
                <div className="flex items-center gap-2">
                  {sandboxPhase === 'running' && (
                    <span className="flex items-center gap-1.5 text-[10px] text-accent-light">
                      <Loader2 className="w-3 h-3 animate-spin" /> Running... {sandboxElapsed > 0 && `${sandboxElapsed}s`}
                    </span>
                  )}
                  {sandboxPhase === 'success' && (
                    <span className="flex items-center gap-1.5 text-[10px] text-success font-semibold">
                      <CheckCircle2 className="w-3.5 h-3.5" /> Build passed
                      {sandboxDuration != null && ` · ${(sandboxDuration / 1000).toFixed(1)}s`}
                    </span>
                  )}
                  {sandboxPhase === 'failed' && (
                    <div className="flex items-center gap-2">
                      <span className="flex items-center gap-1.5 text-[10px] text-danger font-semibold">
                        <Bug className="w-3.5 h-3.5" /> Build failed
                        {sandboxExitCode != null && ` · exit ${sandboxExitCode}`}
                      </span>
                      <button
                        onClick={() => sandboxRunnerRef.current?.(1)}
                        className="flex items-center gap-1 text-[9px] px-2 py-1 rounded border border-border hover:bg-surface-light transition-colors cursor-pointer"
                      >
                        <RefreshCw className="w-2.5 h-2.5" /> Retry
                      </button>
                    </div>
                  )}
                </div>
              </div>

              {/* Terminal output */}
              <div className="bg-black/70 max-h-72 overflow-y-auto p-3 font-mono text-[10px] leading-relaxed">
                {sandboxLogs.map(log => {
                  const lower = log.line.toLowerCase();
                  const color =
                    log.level === 'error' || (log.stream === 'stderr' && /error|exception|fatal|failed/.test(lower))
                      ? 'text-red-400'
                      : /warn/.test(lower)
                        ? 'text-yellow-400'
                        : log.line.startsWith('[repair]')
                          ? 'text-purple-400'
                          : log.line.startsWith('[sandbox]')
                            ? 'text-blue-400'
                            : log.stream === 'stderr'
                              ? 'text-red-300'
                              : 'text-green-300';
                  return (
                    <div key={log.id} className={`${color} whitespace-pre-wrap break-all`}>
                      {log.elapsed && <span className="text-zinc-500 mr-1.5">[{log.elapsed}]</span>}
                      {log.line}
                    </div>
                  );
                })}
                {sandboxPhase === 'running' && (
                  <div className="text-accent-light animate-pulse">▋</div>
                )}
                <div ref={sandboxLogEndRef} />
              </div>

              {/* Summary bar */}
              {(sandboxPhase === 'success' || sandboxPhase === 'failed') && (
                <div className="flex items-center gap-4 px-4 py-2 border-t border-border text-[10px] text-muted">
                  <span>{sandboxLogs.length} lines output</span>
                  {sandboxExitCode != null && <span>Exit code: {sandboxExitCode}</span>}
                  {sandboxDuration != null && <span>{(sandboxDuration / 1000).toFixed(1)}s</span>}
                  <span className="ml-auto">
                    {sandboxLogs.filter(l => l.level === 'error' || (l.stream === 'stderr' && /error|exception/.test(l.line.toLowerCase()))).length} error(s)
                  </span>
                </div>
              )}
            </motion.div>
          )}

          {/* Action Buttons */}
          <div className="flex gap-3">
            <button
              onClick={() => onNavigate('comparison', projectId)}
              className="flex-1 py-2.5 rounded-lg gradient-accent text-white text-xs font-semibold hover:opacity-90 transition-opacity flex items-center justify-center gap-2 cursor-pointer"
            >
              <ArrowRight className="w-3.5 h-3.5" /> Review Generated Code
            </button>
            <button
              onClick={() => onNavigate('verification', projectId)}
              className="flex-1 py-2.5 rounded-lg border border-border text-xs font-semibold hover:bg-surface-light transition-colors flex items-center justify-center gap-2 cursor-pointer"
            >
              <Shield className="w-3.5 h-3.5" /> Verification & QA
            </button>
            <button
              onClick={() => onNavigate('artifacts', projectId)}
              className="flex-1 py-2.5 rounded-lg border border-border text-xs font-semibold hover:bg-surface-light transition-colors flex items-center justify-center gap-2 cursor-pointer"
            >
              <Download className="w-3.5 h-3.5" /> View Artifacts
            </button>
            <button
              onClick={() => onNavigate('export', projectId)}
              className="flex-1 py-2.5 rounded-lg border border-border text-xs font-semibold hover:bg-surface-light transition-colors flex items-center justify-center gap-2 cursor-pointer"
            >
              <Rocket className="w-3.5 h-3.5" /> Export & Deploy
            </button>
            {activeRunId && (
              <button
                onClick={() => setShowReviewBoard(true)}
                className="flex-1 py-2.5 rounded-lg border border-accent/40 text-xs font-semibold text-accent-light hover:bg-accent/10 transition-colors flex items-center justify-center gap-2 cursor-pointer"
              >
                <Eye className="w-3.5 h-3.5" /> Review &amp; Seal Bundle
              </button>
            )}
          </div>

          {showReviewBoard && activeRunId && (
            <ReviewBoard
              runId={activeRunId}
              projectName={project?.name}
              onBack={() => setShowReviewBoard(false)}
            />
          )}
        </motion.div>
        );
      })()}

      {/* Failed State */}
      {currentPhase === 'failed' && (() => {
        const errList = conversionResult?.metadata !== undefined &&
          typeof (conversionResult as { metadata?: { errors?: unknown } }).metadata === 'object'
            ? (conversionResult as { metadata?: { errors?: string[] } }).metadata?.errors
            : undefined;
        const allErrs = Array.isArray(errList) ? errList.filter((e): e is string => typeof e === 'string') : [];
        const rawFromMeta = allErrs[0];
        const rawFromLogs = inferPrimaryFailureFromLogs(logs);
        const rawErr = rawFromMeta ?? rawFromLogs;
        const ux = classifyConversionFailure(rawErr);
        const errCategory = categorizeConversionErrors(allErrs.length > 0 ? allErrs : rawFromLogs ? [rawFromLogs] : []);
        const failedConvId = (conversionResult as { conversionId?: string })?.conversionId ?? activeConversionId;
        const restErrs = allErrs.slice(1).filter((e) => e.trim().length > 0);

        return (
        <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="glass-light rounded-lg p-6 border border-red-500/30">
          <div className="flex items-center gap-4 mb-6">
            <div className="w-16 h-16 rounded-full bg-red-500/15 flex items-center justify-center">
              <AlertTriangle className="w-8 h-8 text-danger" />
            </div>
            <div>
              <h3 className="text-lg font-semibold text-foreground">{ux.title}</h3>
              <p className="text-sm text-muted leading-relaxed">{ux.detail}</p>
              <div className="flex flex-wrap items-center gap-2 mt-2">
                <span className="text-[10px] text-muted uppercase tracking-wider">{ux.kind.replace(/-/g, ' ')} issue</span>
                {errCategory === 'preflight' && (
                  <span className="text-[9px] font-semibold px-1.5 py-px rounded bg-amber-500/15 text-amber-400">Check source files or language pair</span>
                )}
                {errCategory === 'size-guard' && (
                  <span className="text-[9px] font-semibold px-1.5 py-px rounded bg-amber-500/15 text-amber-400">Contact support for a higher plan</span>
                )}
                {errCategory === 'quality-gate' && (
                  <span className="text-[9px] font-semibold px-1.5 py-px rounded bg-amber-500/15 text-amber-400">Retry at lower quality level or analyze errors</span>
                )}
                {errCategory === 'runtime' && failedConvId && (
                  <span className="text-[9px] font-mono text-muted">ID: {failedConvId}</span>
                )}
              </div>
            </div>
          </div>

          {restErrs.length > 0 ? (
            <div className="mb-6 rounded-md border border-border bg-surface-light/80 p-3">
              <p className="text-[10px] text-muted uppercase tracking-wider mb-2">Additional messages</p>
              <ul className="text-xs text-muted space-y-1 list-disc pl-4">
                {restErrs.map((e, i) => (
                  <li key={i}>{e}</li>
                ))}
              </ul>
            </div>
          ) : null}
          
          <div className="space-y-2 mb-6">
            {steps.filter(s => s.status === 'failed').map(step => (
              <div key={step.id} className="glass-light rounded-md p-3 flex items-center gap-3">
                <X className="w-4 h-4 text-danger" />
                <div>
                  <p className="text-xs font-medium text-foreground">{step.name}</p>
                  <p className="text-[10px] text-muted">{step.description}</p>
                </div>
              </div>
            ))}
          </div>
          
          {/* Run-lost CTA */}
          {runLost && (
            <div className="mb-4 rounded-lg border border-amber-400/30 bg-amber-500/10 p-4">
              <p className="text-sm font-semibold text-amber-400 mb-1">Run result lost</p>
              <p className="text-xs text-muted mb-3">
                The engine completed the run but the result was not persisted (the engine may have crashed before flushing). The translation output is unavailable.
              </p>
              <button
                onClick={handleRetry}
                className="text-xs font-semibold text-amber-400 border border-amber-400/40 rounded px-3 py-1.5 hover:bg-amber-500/15 transition-colors cursor-pointer"
              >
                Retry from start
              </button>
            </div>
          )}

          {/* BudgetExceeded CTA */}
          {budgetExceeded && (
            <div className="mb-4 rounded-lg border border-amber-400/30 bg-amber-500/10 p-4">
              <p className="text-sm font-semibold text-amber-400 mb-1">Run paused at budget cap</p>
              <p className="text-xs text-muted mb-3">
                {budgetExceeded.message ?? 'The per-run budget cap was reached. The partial output may be usable.'}
              </p>
              <button
                onClick={() => {
                  const cfg = parseProjectConfig(project);
                  const prevCap = typeof cfg.maxCostUsd === 'number' && cfg.maxCostUsd > 0 ? cfg.maxCostUsd : null;
                  const newCap = prevCap ? prevCap * 2 : 10;
                  api.updateProject(projectId, { config: { maxCostUsd: newCap } }).then(() => {
                    setBudgetExceeded(null);
                    handleRetry();
                  }).catch(() => handleRetry());
                }}
                className="text-xs font-semibold text-amber-400 border border-amber-400/40 rounded px-3 py-1.5 hover:bg-amber-500/15 transition-colors cursor-pointer"
              >
                Resume with 2× cap
              </button>
            </div>
          )}

          {/* Quota error banner */}
          {quotaError && (
            <div className="mb-4 rounded-lg border border-red-500/30 bg-red-500/10 p-4">
              <p className="text-sm font-semibold text-red-400 mb-1">
                {quotaError.reason === 'concurrency-cap' ? 'Concurrency limit reached' : 'Rate limit reached'}
              </p>
              {quotaError.reason === 'rate-limit' && quotaError.observed && quotaError.limits && (
                <p className="text-xs text-muted mb-2">
                  {quotaError.observed.runs_last_hour ?? '?'} run{(quotaError.observed.runs_last_hour ?? 0) !== 1 ? 's' : ''} in the last hour (limit: {quotaError.limits.runs_per_hour ?? '?'}).
                </p>
              )}
              {quotaError.reason === 'concurrency-cap' && quotaError.observed && quotaError.limits && (
                <p className="text-xs text-muted mb-2">
                  {quotaError.observed.in_flight ?? '?'} run{(quotaError.observed.in_flight ?? 0) !== 1 ? 's' : ''} currently active (max: {quotaError.limits.max_concurrent ?? '?'}).
                  Wait for one to finish or cancel an existing run.
                </p>
              )}
              {quotaCountdown > 0 ? (
                <p className="text-xs text-muted">
                  Retry available in <span className="text-red-400 font-semibold tabular-nums">{Math.floor(quotaCountdown / 60)}:{String(quotaCountdown % 60).padStart(2, '0')}</span>
                </p>
              ) : (
                <button
                  onClick={() => { setQuotaError(null); handleRetry(); }}
                  className="mt-2 text-xs font-semibold text-red-400 border border-red-400/40 rounded px-3 py-1.5 hover:bg-red-500/15 transition-colors cursor-pointer"
                >
                  Try again
                </button>
              )}
            </div>
          )}

          {!runLost && !budgetExceeded && (
          <button
            onClick={handleRetry}
            className="w-full py-3 rounded-lg gradient-accent text-white text-sm font-semibold hover:opacity-90 transition-opacity flex items-center justify-center gap-2 cursor-pointer"
          >
            <Play className="w-4 h-4" /> Restart Migration
          </button>
          )}
        </motion.div>
        );
      })()}
    </div>
  );
}
