/**
 * audit-dev-frontend/apply.ts — Mechanical rewriter for autoFixable findings.
 *
 * Each fix function is `fix(input) → string | null`. Returns null = couldn't
 * apply safely (file parse error, ambiguous context). Atomicity per file:
 * if any fix throws on a file, NO writes happen for that file.
 *
 * Two classes of fixes:
 * - In-place patches (DEV-UI-002, 004, 005, 011): rewrite the file source.
 * - Delegate to a sibling generator CLI (DEV-UI-001, 007, 009): re-run the
 *   scaffold CLI with --overwrite (mirrors ui-polish/apply.ts:324-409).
 */
import path from 'node:path';
import { exec } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { mkdir, writeFile } from 'node:fs/promises';
import { readText, writeText, fileExists } from '../../../../lib/fs.js';
import {
  appendRegistryEntry,
  fixPageTemplateWrap,
  fixPermissionGuardWrap,
} from '../../../../lib/frontend-fixers.js';
import { audit, buildInventory } from './audit.js';
import type {
  ApplyReport,
  AuditDevFrontendArgs,
  Finding,
  PageEntry,
} from './types.js';

// ─── In-place fix dispatchers ─────────────────────────────────────────────

interface InPlaceFix {
  /** Returns the patched source, null if fix couldn't apply, or the unchanged
   *  source if the fix is a no-op. */
  apply: (source: string, finding: Finding, args: AuditDevFrontendArgs) => string | null;
}

const IN_PLACE: Record<string, InPlaceFix> = {
  'DEV-UI-005': {
    apply: (source) => fixPageTemplateWrap(source),
  },
  'DEV-UI-011': {
    apply: (source, finding) => {
      const perm = (finding.params as { permission?: string }).permission;
      if (!perm) return null;
      return fixPermissionGuardWrap(source, perm);
    },
  },
};

// DEV-UI-002 (orphan page) and DEV-UI-004 (missing i18n) target *different*
// files than the finding.file (they write registries / locale stubs). Handled
// separately, not via IN_PLACE map.

// ─── Delegate-to-CLI fixes ────────────────────────────────────────────────

interface DelegateResult {
  ruleId: string;
  target: string;
  success: boolean;
  reason: string;
}

/**
 * Locate a sibling scaffold CLI relative to this file.
 * Mirrors ui-polish/apply.ts:344 path math.
 */
function locateSiblingCli(relPath: string): string {
  const thisFile = fileURLToPath(import.meta.url);
  return path.resolve(path.dirname(thisFile), relPath);
}

async function runScaffoldComponent(args: AuditDevFrontendArgs, finding: Finding): Promise<DelegateResult> {
  const cli = locateSiblingCli('../../../frontend/component/cli/scaffold-component/index.ts');
  if (!(await fileExists(cli))) {
    return { ruleId: finding.code, target: finding.file ?? '', success: false, reason: `scaffold-component CLI not found at ${cli}` };
  }
  // Build a minimal spec — params should carry entity/kind etc., but if not,
  // we'd need PRD slice access. For now we only delegate when params hint at the entity.
  const params = finding.params as { missing?: string };
  if (!params.missing) {
    return { ruleId: finding.code, target: '', success: false, reason: 'no missing entity/kind in finding params' };
  }
  // Each missing entry is "entity/kind". We invoke once per missing.
  const entries = params.missing.split(',').map((s) => s.trim()).filter(Boolean);
  const failures: string[] = [];
  for (const entry of entries) {
    const [entity, kind] = entry.split('/');
    if (!entity || !kind) continue;
    const spec = {
      module: args.moduleCode,
      entity,
      section: entity.toLowerCase(),
      appCode: args.appCode,
      views: [kind],
      fields: [{ name: 'id', type: 'string', required: true }],
      projectPath: args.projectPath,
    };
    try {
      await execCli(cli, ['--spec', JSON.stringify(spec), '--overwrite']);
    } catch (err) {
      failures.push(`${entry}: ${err instanceof Error ? err.message : String(err)}`);
    }
  }
  return failures.length === 0
    ? { ruleId: finding.code, target: params.missing, success: true, reason: `scaffold-component executed for ${entries.length} entry(ies)` }
    : { ruleId: finding.code, target: params.missing, success: false, reason: failures.join(' | ') };
}

async function runScaffoldTests(args: AuditDevFrontendArgs, finding: Finding): Promise<DelegateResult> {
  const cli = locateSiblingCli('../../../testing/cli/scaffold-tests/generate.ts');
  const altCli = locateSiblingCli('../../../testing/cli/scaffold-frontend-tests/index.ts');
  const targetCli = (await fileExists(cli)) ? cli : (await fileExists(altCli)) ? altCli : null;
  if (!targetCli) {
    return { ruleId: finding.code, target: finding.file ?? '', success: false, reason: `scaffold-tests CLI not found (tried ${cli} and ${altCli})` };
  }
  const target = finding.file;
  if (!target) {
    return { ruleId: finding.code, target: '', success: false, reason: 'finding has no file path' };
  }
  try {
    await execCli(targetCli, ['--project-path', args.projectPath, '--layer', 'frontend', '--target', target]);
    return { ruleId: finding.code, target, success: true, reason: 'scaffold-tests executed' };
  } catch (err) {
    return { ruleId: finding.code, target, success: false, reason: err instanceof Error ? err.message : String(err) };
  }
}

function execCli(cliPath: string, extraArgs: string[]): Promise<void> {
  const quote = (s: string) =>
    process.platform === 'win32' ? `"${s.replace(/"/g, '\\"')}"` : `'${s.replace(/'/g, `'\\''`)}'`;
  return new Promise((resolve, reject) => {
    const cmd = `npx --prefer-offline tsx ${quote(cliPath)} ${extraArgs.map(quote).join(' ')}`;
    const child = exec(cmd, { maxBuffer: 8 * 1024 * 1024, timeout: 60_000 });
    let stderr = '';
    child.stderr?.on('data', (d: Buffer | string) => { stderr += String(d); });
    child.on('close', (code) => {
      if (code === 0) resolve();
      else reject(new Error(`CLI exit=${code}: ${stderr.slice(-500)}`));
    });
    child.on('error', reject);
  });
}

// ─── DEV-UI-002 fix: append registry entry ────────────────────────────────

async function fixOrphanPage(args: AuditDevFrontendArgs, finding: Finding): Promise<{ filesModified: string[]; ok: boolean; reason: string }> {
  const orphanFile = finding.file;
  if (!orphanFile) return { filesModified: [], ok: false, reason: 'no file in finding' };
  const projectPath = path.resolve(args.projectPath);
  // Derive component key from path: /pages/{module}/{section}/<name>Page.tsx → {appCode}.{module}.{section}
  const norm = orphanFile.replace(/\\/g, '/');
  const match = /\/pages\/([^/]+)\/([^/]+)\/[^/]+Page\.tsx$/.exec(norm);
  if (!match) return { filesModified: [], ok: false, reason: 'page path does not match /pages/{module}/{section}/...Page.tsx' };
  const [, mod, section] = match;
  const componentKey = `${args.appCode}.${mod}.${section}`;
  const importPath = '@/' + norm.replace(/^src\//, '').replace(/\.tsx$/, '');

  // Find or create the module registry: src/extensions/{module}Registry.ts
  const inventory = await buildInventory(projectPath);
  const targetReg = inventory.registries.find((r) => r.file.endsWith(`${mod}Registry.ts`));
  let regAbs: string;
  let regSource: string;
  if (targetReg) {
    regAbs = path.resolve(projectPath, targetReg.file);
    regSource = await readText(regAbs);
  } else {
    regAbs = path.resolve(projectPath, 'src', 'extensions', `${mod}Registry.ts`);
    regSource = `import { PageRegistry, lazyWithRetry } from '@atlashub/smartstack';\n`;
  }
  const newSource = appendRegistryEntry(regSource, componentKey, importPath);
  if (newSource === regSource) return { filesModified: [], ok: true, reason: 'registry already contains entry' };
  await writeText(regAbs, newSource);
  return { filesModified: [path.relative(projectPath, regAbs).replace(/\\/g, '/')], ok: true, reason: `appended ${componentKey} to ${path.basename(regAbs)}` };
}

// ─── DEV-UI-004 fix: write empty namespace JSON for missing locales ───────

async function fixMissingI18n(args: AuditDevFrontendArgs, finding: Finding): Promise<{ filesModified: string[]; ok: boolean; reason: string }> {
  const params = finding.params as { missing?: string; expectedNamespace?: string };
  if (!params.missing || !params.expectedNamespace) {
    return { filesModified: [], ok: false, reason: 'no missing locale.namespace pairs in finding params' };
  }
  const projectPath = path.resolve(args.projectPath);
  const created: string[] = [];
  for (const pair of params.missing.split(',').map((s) => s.trim()).filter(Boolean)) {
    const dotIdx = pair.indexOf('.');
    if (dotIdx === -1) continue;
    const locale = pair.slice(0, dotIdx);
    const ns = pair.slice(dotIdx + 1);
    // Try src/locales first, fallback to src/i18n/locales
    const candidates = [
      path.join(projectPath, 'src', 'locales', locale, `${ns}.json`),
      path.join(projectPath, 'src', 'i18n', 'locales', locale, `${ns}.json`),
    ];
    const target = candidates[0]; // create under src/locales by default
    if (await fileExists(target)) continue;
    await mkdir(path.dirname(target), { recursive: true });
    await writeFile(target, '{}\n', 'utf-8');
    created.push(path.relative(projectPath, target).replace(/\\/g, '/'));
  }
  return { filesModified: created, ok: true, reason: `created ${created.length} empty namespace JSON file(s)` };
}

// ─── Regeneration pass (page-level rebuild via scaffold-component) ────────

const PAGE_FILENAME_RE = /^([A-Z]\w+?)(List|Detail|Form|Dashboard|Create|Edit|Home)Page\.tsx$/;

async function regeneratePagesAboveThreshold(
  args: AuditDevFrontendArgs,
  findings: Finding[],
): Promise<Array<{ file: string; regenerated: boolean; reason: string }>> {
  const threshold = args.regenerateThreshold;
  if (threshold <= 0) return [];

  const byFile = new Map<string, Finding[]>();
  for (const f of findings) {
    if (f.severity !== 'err' || !f.file) continue;
    const arr = byFile.get(f.file) ?? [];
    arr.push(f);
    byFile.set(f.file, arr);
  }

  const results: Array<{ file: string; regenerated: boolean; reason: string }> = [];
  for (const [file, errs] of byFile) {
    if (errs.length < threshold) continue;
    if (!PAGE_FILENAME_RE.test(path.basename(file))) {
      results.push({ file, regenerated: false, reason: 'not a *Page.tsx file' });
      continue;
    }
    // Delegate via DEV-UI-001 spec (entity/kind from filename).
    const m = PAGE_FILENAME_RE.exec(path.basename(file))!;
    const fakeFinding: Finding = {
      dimension: 'devFrontend',
      code: 'DEV-UI-001',
      severity: 'err',
      label: 'DEV_UI_001_err',
      params: { missing: `${m[1]}/${m[2].toLowerCase()}` },
      solution: '',
      autoFixable: true,
    };
    const r = await runScaffoldComponent(args, fakeFinding);
    results.push({ file, regenerated: r.success, reason: r.reason });
  }
  return results;
}

// ─── Entry point ──────────────────────────────────────────────────────────

export async function apply(args: AuditDevFrontendArgs): Promise<ApplyReport> {
  // Phase 1 — audit to know what to fix.
  const report = await audit(args);
  const projectPath = path.resolve(args.projectPath);

  const skipped: Finding[] = [];
  const filesModified = new Set<string>();
  let fixesApplied = 0;

  // ─── In-place fixes (per-file atomicity) ─────────────────────────────
  // Group autofixable findings by file (only for rules that mutate the page itself).
  const inPlaceByFile = new Map<string, Finding[]>();
  for (const f of report.findings) {
    if (!f.autoFixable || f.severity === 'ok') continue;
    if (!IN_PLACE[f.code]) continue;
    if (!f.file) continue;
    const arr = inPlaceByFile.get(f.file) ?? [];
    arr.push(f);
    inPlaceByFile.set(f.file, arr);
  }

  for (const [relFile, findings] of inPlaceByFile) {
    const abs = path.resolve(projectPath, relFile);
    let source: string;
    try {
      source = await readText(abs);
    } catch {
      skipped.push(...findings);
      continue;
    }
    const original = source;
    let allOk = true;
    for (const finding of findings) {
      try {
        const patched = IN_PLACE[finding.code].apply(source, finding, args);
        if (patched === null) {
          skipped.push(finding);
          continue;
        }
        if (patched !== source) fixesApplied++;
        source = patched;
      } catch {
        allOk = false;
        skipped.push(finding);
        break;
      }
    }
    if (allOk && source !== original) {
      await writeText(abs, source);
      filesModified.add(relFile);
    }
  }

  // ─── DEV-UI-002 (orphan page) — writes registry files ────────────────
  for (const f of report.findings) {
    if (f.code !== 'DEV-UI-002' || !f.autoFixable || f.severity === 'ok') continue;
    try {
      const r = await fixOrphanPage(args, f);
      if (r.ok) {
        for (const m of r.filesModified) filesModified.add(m);
        fixesApplied += r.filesModified.length;
      } else {
        skipped.push(f);
      }
    } catch {
      skipped.push(f);
    }
  }

  // ─── DEV-UI-004 (missing i18n) — writes empty JSON stubs ─────────────
  for (const f of report.findings) {
    if (f.code !== 'DEV-UI-004' || !f.autoFixable || f.severity === 'ok') continue;
    try {
      const r = await fixMissingI18n(args, f);
      if (r.ok && r.filesModified.length > 0) {
        for (const m of r.filesModified) filesModified.add(m);
        fixesApplied += r.filesModified.length;
      } else if (!r.ok) {
        skipped.push(f);
      }
    } catch {
      skipped.push(f);
    }
  }

  // ─── Delegate-to-CLI fixes (DEV-UI-001, 007, 009) ────────────────────
  for (const f of report.findings) {
    if (!f.autoFixable || f.severity === 'ok') continue;
    if (f.code === 'DEV-UI-001' || f.code === 'DEV-UI-009') {
      try {
        const r = await runScaffoldComponent(args, f);
        if (r.success) fixesApplied++;
        else skipped.push(f);
      } catch {
        skipped.push(f);
      }
    } else if (f.code === 'DEV-UI-007') {
      try {
        const r = await runScaffoldTests(args, f);
        if (r.success) fixesApplied++;
        else skipped.push(f);
      } catch {
        skipped.push(f);
      }
    }
  }

  // ─── Phase 2 — regeneration pass: rebuild pages with too many residual errors
  const secondAudit = await audit(args);
  const regenerations = await regeneratePagesAboveThreshold(args, secondAudit.findings);
  for (const r of regenerations) {
    if (r.regenerated) filesModified.add(r.file);
  }

  // Final audit to populate the report with current state.
  const finalReport = await audit(args);
  return {
    ...finalReport,
    fixesApplied,
    filesModified: [...filesModified],
    fixesSkipped: skipped,
    regenerations,
  };
}
