"use strict";

/**
 * cmd-upgrade-framework.js — `qa-stlc upgrade-framework`
 *
 * Refreshes the self-healing framework files (src/utils/locators/*.ts and
 * src/utils/storage-state/*.ts) in an existing scaffolded repo from the
 * embedded boilerplate-bundle.js. Diff-aware and non-destructive by default:
 *
 *   qa-stlc upgrade-framework             # dry-run — print drifted/missing files
 *   qa-stlc upgrade-framework --apply     # write bundle versions, .bak originals
 *   qa-stlc upgrade-framework --include "src/hooks/**"   # widen the scope
 *
 * Use case: a project was scaffolded months ago with an older agent build
 * whose LocatorRepository or LocatorHealer had a bug since fixed upstream.
 * `qa-stlc init` updates skills / MCP config but never touches src/, so
 * those bugs persist forever. This command is the explicit upgrade path.
 *
 * Safety:
 *   • Default is dry-run — nothing on disk changes.
 *   • --apply writes each replaced file's original content to
 *     <path>.bak.<timestamp> first so the change is reversible.
 *   • Files outside the framework scope are NEVER touched.
 */

const fs   = require("fs");
const path = require("path");

const BUNDLE = require("./boilerplate-bundle");
const {
  DEFAULT_SCOPE,
  checkFrameworkUpgradeCompatibility,
  formatCompatibilityIssues,
  matchesScope,
} = require("./framework-upgrade-support");

// Color helpers — match cmd-init.js style.
const C = {
  reset: "\x1b[0m", bold: "\x1b[1m",
  green: "\x1b[32m", cyan: "\x1b[36m",
  yellow: "\x1b[33m", red: "\x1b[31m", dim: "\x1b[2m",
};
const ok   = (m) => console.log(`${C.green}✓${C.reset}  ${m}`);
const info = (m) => console.log(`${C.cyan}→${C.reset}  ${m}`);
const warn = (m) => console.log(`${C.yellow}⚠${C.reset}  ${m}`);
const die  = (m) => { console.error(`${C.red}✗${C.reset}  ${m}`); process.exit(1); };

/**
 * Default scope: only the canonical self-healing framework files. These are
 * the ones most likely to drift in ways that silently break heal persistence
 * (see the LocatorRepository upsert short-circuit bug). Hooks and locator
 * files are excluded by default — they are typically customized per project.
 */
/**
 * Compare the on-disk content against the bundle content. Returns one of:
 *   "match"   — identical (no action needed)
 *   "drift"   — exists on disk but content differs
 *   "missing" — bundle has it, disk doesn't
 */
function classify(absPath, bundleContent) {
  if (!fs.existsSync(absPath)) return "missing";
  try {
    const onDisk = fs.readFileSync(absPath, "utf-8");
    return onDisk === bundleContent ? "match" : "drift";
  } catch {
    return "drift";
  }
}

/**
 * Detect whether the target dir looks like a scaffolded QA project at all.
 * Avoids running this command in an unrelated repo where dropping files
 * would be obviously wrong. Single sentinel: src/utils/locators/ exists.
 */
function looksScaffolded(rootDir) {
  return fs.existsSync(path.join(rootDir, "src", "utils", "locators"));
}

/**
 * Public entry — wired in qa-stlc.js.
 */
module.exports = async function upgradeFramework(opts = {}) {
  const apply  = Boolean(opts.apply);
  const rootDir = path.resolve(opts.dir || process.cwd());

  console.log(`\n${C.bold}QA STLC Agents — framework upgrade${C.reset}\n`);
  info(`Target: ${rootDir}`);
  info(`Mode  : ${apply ? `${C.bold}apply${C.reset} (writes files, backs up originals)` : "dry-run (no writes)"}`);

  if (!looksScaffolded(rootDir)) {
    die(
      `No src/utils/locators/ directory at ${rootDir}.\n` +
      `  This command only runs on repos previously scaffolded by qa-stlc.\n` +
      `  Use 'qa-stlc scaffold' to bootstrap a new project.`
    );
  }

  const compatibility = checkFrameworkUpgradeCompatibility(rootDir);
  if (!compatibility.ok) {
    die(
      `This repo has src/utils/locators/, but it is missing scaffold prerequisites required by the bundled framework files.\n` +
      `${formatCompatibilityIssues(compatibility)}\n` +
      `  qa-stlc upgrade-framework only refreshes an existing qa-stlc scaffold.\n` +
      `  For an existing Playwright suite, use 'stlc-migrate --source <existing-project> --helix <target>' instead.`
    );
  }

  // Build the scope. Extra includes accepted as --include regex strings;
  // keep the default scope narrow so accidents are bounded.
  const scope = [...DEFAULT_SCOPE];
  for (const extra of (opts.include ? [].concat(opts.include) : [])) {
    try { scope.push(new RegExp(extra)); }
    catch (e) { warn(`Skipping invalid --include regex "${extra}": ${e.message}`); }
  }

  const drift   = [];
  const missing = [];
  const matched = [];
  for (const [relPath, content] of Object.entries(BUNDLE)) {
    if (!matchesScope(relPath, scope)) continue;
    if (relPath.endsWith(".gitkeep")) continue;
    const absPath = path.join(rootDir, relPath);
    const status  = classify(absPath, content);
    if (status === "drift") drift.push(relPath);
    else if (status === "missing") missing.push(relPath);
    else matched.push(relPath);
  }

  console.log("");
  ok(`In sync : ${matched.length}`);
  if (drift.length)   warn(`Drifted : ${drift.length}`);
  if (missing.length) warn(`Missing : ${missing.length}`);

  if (!drift.length && !missing.length) {
    console.log(`\n${C.green}Framework files match the bundled version. Nothing to upgrade.${C.reset}\n`);
    return;
  }

  // Show the list so the user knows exactly what would change.
  if (drift.length) {
    console.log(`\n${C.bold}Drifted (would overwrite, originals backed up to .bak.<ts>):${C.reset}`);
    for (const p of drift) console.log(`  ${C.yellow}~${C.reset} ${p}`);
  }
  if (missing.length) {
    console.log(`\n${C.bold}Missing (would be added):${C.reset}`);
    for (const p of missing) console.log(`  ${C.green}+${C.reset} ${p}`);
  }

  if (!apply) {
    console.log(
      `\n${C.dim}Dry-run complete. Re-run with --apply to write these changes.${C.reset}\n`
    );
    return;
  }

  // ── Apply phase ──────────────────────────────────────────────────────────
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
  let wrote = 0, backedUp = 0;
  for (const relPath of [...drift, ...missing]) {
    const absPath  = path.join(rootDir, relPath);
    const bundleContent = BUNDLE[relPath];
    try {
      fs.mkdirSync(path.dirname(absPath), { recursive: true });
      if (fs.existsSync(absPath)) {
        const bakPath = `${absPath}.bak.${ts}`;
        fs.copyFileSync(absPath, bakPath);
        backedUp++;
      }
      fs.writeFileSync(absPath, bundleContent, "utf-8");
      wrote++;
    } catch (e) {
      warn(`Could not upgrade ${relPath}: ${e.message}`);
    }
  }

  console.log("");
  ok(`${wrote} file(s) written, ${backedUp} original(s) backed up.`);
  console.log(
    `\n${C.bold}Next step:${C.reset} run your test suite to verify the upgraded framework wires up cleanly:`
  );
  console.log(`  ${C.cyan}npx tsc --noEmit${C.reset}`);
  console.log(`  ${C.cyan}npm test${C.reset}\n`);
  console.log(
    `${C.dim}To revert: restore the .bak.${ts} files alongside each upgraded file.${C.reset}\n`
  );
};
