/**
 * upgrade-to-2: orchestrator codemod that chains every step required to move
 * a Project Zero brand project from pz-next 1.x to 2.0 stable.
 *
 * Steps (in order):
 *   1.  Preflight checks (git state, package.json, Node.js project)
 *   2.  Create a backup git branch (pre-upgrade-2-YYYYMMDD)
 *   3.  Bump package.json dependencies to the 2.0 target matrix
 *   4.  yarn clean && yarn install
 *   5.  Run @next/codemod upgrade (Next 16 + React 19)
 *   6.  Run migrate-auth-v5 codemod (next-auth v4 -> v5)
 *   7.  Run sentry-9 codemod (only when @sentry/nextjs is present)
 *   8.  Run migrate-eslint codemod (ESLint v9 flat config)
 *   9.  yarn build (validation)
 *   10. Summary report
 *
 * Out of scope in this first version:
 *   - Tailwind v3 -> v4 migration (run manually with @tailwindcss/upgrade)
 *   - migrate-segments codemod (separate task)
 *
 * Usage:
 *   cd <brand-root>
 *   npx @akinon/projectzero codemod --codemod=upgrade-to-2 [flags]
 *
 *   Flags:
 *     --dry-run             Preview only. No package.json writes, no install,
 *                           codemods invoked in dry-run where supported.
 *     --skip-preflight      Skip git state / package.json checks
 *     --skip-backup         Do not create pre-upgrade-2-* branch
 *     --skip-bump           Do not modify package.json
 *     --skip-next-codemod   Do not run @next/codemod upgrade
 *     --skip-auth           Do not run migrate-auth-v5
 *     --skip-sentry         Do not run sentry-9 (auto-skipped if not needed)
 *     --skip-eslint         Do not run migrate-eslint
 *     --skip-install        Do not run yarn install
 *     --skip-build          Do not run yarn build
 *     --interactive         Pause for confirmation after each step
 *     --verbose             Extra logs
 */

const path = require('path');
const fs = require('fs');
const { execSync, spawnSync } = require('child_process');
const readline = require('readline');

const AUTH_CODEMOD = path.resolve(__dirname, '..', 'migrate-auth-v5');
const SENTRY_CODEMOD = path.resolve(__dirname, '..', 'sentry-9');
const ESLINT_CODEMOD = path.resolve(__dirname, '..', 'migrate-eslint');

const TARGET_MATRIX = {
  dependencies: {
    next: '16.2.4',
    react: '19.2.5',
    'react-dom': '19.2.5',
    'next-auth': '5.0.0-beta.25',
    '@akinon/next': 'latest'
  },
  devDependencies: {
    '@types/react': '19.2.14',
    '@types/react-dom': '19.2.3',
    'eslint-config-next': '16.2.4',
    eslint: '9.39.4',
    'eslint-config-prettier': '10.1.1',
    tailwindcss: '3.4.17',
    '@akinon/projectzero': 'latest',
    '@akinon/eslint-plugin-projectzero': 'latest',
    '@akinon/tsconfig': 'latest'
  },
  conditional: {
    '@sentry/nextjs': '^9.0.0'
  }
};

// Packages removed when migrating to ESLint v9 / flat config; their rules are
// now provided by `eslint-config-next/typescript`.
const DEP_DROP = [
  '@typescript-eslint/eslint-plugin',
  '@typescript-eslint/parser'
];

const STEP_COLUMN_WIDTH = 32;

function log(msg) {
  const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
  console.log(`[${ts}] ${msg}`);
}

function logStep(step, msg) {
  const padded = step.padEnd(STEP_COLUMN_WIDTH, ' ');
  log(`${padded} ${msg}`);
}

function runCommand(cmd, { cwd, dryRun, verbose }) {
  if (dryRun) {
    log(`[DRY] ${cmd}`);
    return { status: 0, dry: true };
  }
  if (verbose) log(`$ ${cmd}`);
  try {
    execSync(cmd, { cwd, stdio: 'inherit' });
    return { status: 0 };
  } catch (err) {
    return { status: err.status || 1, error: err };
  }
}

function confirm(question) {
  return new Promise((resolve) => {
    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout
    });
    rl.question(`${question} [Y/n] `, (answer) => {
      rl.close();
      resolve(answer.trim().toLowerCase() !== 'n');
    });
  });
}

function parseArgs(argv) {
  const flags = {
    dryRun: argv.includes('--dry-run'),
    skipPreflight: argv.includes('--skip-preflight'),
    skipBackup: argv.includes('--skip-backup'),
    skipBump: argv.includes('--skip-bump'),
    skipNextCodemod: argv.includes('--skip-next-codemod'),
    skipAuth: argv.includes('--skip-auth'),
    skipSentry: argv.includes('--skip-sentry'),
    skipEslint: argv.includes('--skip-eslint'),
    skipInstall: argv.includes('--skip-install'),
    skipBuild: argv.includes('--skip-build'),
    interactive: argv.includes('--interactive'),
    verbose: argv.includes('--verbose')
  };
  return flags;
}

function readPackageJson(cwd) {
  const pkgPath = path.join(cwd, 'package.json');
  if (!fs.existsSync(pkgPath)) return null;
  return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
}

function writePackageJson(cwd, pkg) {
  const pkgPath = path.join(cwd, 'package.json');
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
}

function stepPreflight(cwd, flags) {
  logStep('[1/10] Preflight', 'starting');

  const pkg = readPackageJson(cwd);
  if (!pkg) {
    throw new Error('No package.json found in working directory');
  }

  if (!pkg.dependencies?.next && !pkg.devDependencies?.next) {
    throw new Error('This does not appear to be a Next.js project (no `next` dependency)');
  }

  const gitStatus = spawnSync('git', ['status', '--porcelain'], {
    cwd,
    encoding: 'utf-8'
  });
  if (gitStatus.status !== 0) {
    throw new Error('git not available or not a git repo');
  }
  const dirty = gitStatus.stdout.trim().length > 0;
  if (dirty && !flags.dryRun) {
    throw new Error(
      'Working directory has uncommitted changes. Commit or stash before running upgrade-to-2.'
    );
  }

  const currentNext =
    pkg.dependencies?.next || pkg.devDependencies?.next || 'unknown';
  const currentReact = pkg.dependencies?.react || 'unknown';
  const currentAuth = pkg.dependencies?.['next-auth'] || 'unknown';
  const currentAkinon = pkg.dependencies?.['@akinon/next'] || 'unknown';

  log(`  Current: next@${currentNext} react@${currentReact} next-auth@${currentAuth} @akinon/next@${currentAkinon}`);
  log(`  Target:  next@${TARGET_MATRIX.dependencies.next} react@${TARGET_MATRIX.dependencies.react} next-auth@${TARGET_MATRIX.dependencies['next-auth']} @akinon/next@${TARGET_MATRIX.dependencies['@akinon/next']}`);

  logStep('[1/10] Preflight', 'ok');
}

function stepBackup(cwd, flags) {
  if (flags.skipBackup) {
    logStep('[2/10] Backup branch', 'skipped');
    return null;
  }
  const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
  const branchName = `pre-upgrade-2-${today}`;

  if (flags.dryRun) {
    logStep('[2/10] Backup branch', `[DRY] git branch ${branchName}`);
    return branchName;
  }

  const existing = spawnSync(
    'git',
    ['rev-parse', '--verify', branchName],
    { cwd, encoding: 'utf-8' }
  );
  if (existing.status === 0) {
    logStep('[2/10] Backup branch', `already exists: ${branchName}`);
    return branchName;
  }

  const result = spawnSync('git', ['branch', branchName], {
    cwd,
    stdio: 'inherit'
  });
  if (result.status !== 0) {
    throw new Error(`Failed to create backup branch ${branchName}`);
  }
  logStep('[2/10] Backup branch', `created ${branchName}`);
  return branchName;
}

function bumpDepsSection(section, matrix) {
  const changes = [];
  if (!section) return { section: section || {}, changes };
  for (const [name, version] of Object.entries(matrix)) {
    if (section[name] !== undefined && section[name] !== version) {
      changes.push(`${name}: ${section[name]} -> ${version}`);
      section[name] = version;
    }
  }
  return { section, changes };
}

function dropDeps(pkg, names) {
  const changes = [];
  for (const name of names) {
    if (pkg.dependencies && pkg.dependencies[name]) {
      delete pkg.dependencies[name];
      changes.push(`removed dependencies.${name}`);
    }
    if (pkg.devDependencies && pkg.devDependencies[name]) {
      delete pkg.devDependencies[name];
      changes.push(`removed devDependencies.${name}`);
    }
  }
  return changes;
}

function pinNextScriptsToWebpack(pkg) {
  // Next 16 defaults build/dev to Turbopack, which surfaces bundle-time
  // errors for Node APIs used on the edge runtime (e.g. CompressionStream
  // inside @akinon/next/lib/cache.ts). pz-next itself ships with
  // `next dev --webpack` / `next build --webpack` until those are fixed;
  // brand projects need the same flag to build successfully.
  const changes = [];
  if (!pkg.scripts) return changes;

  ['dev', 'build'].forEach((scriptName) => {
    const current = pkg.scripts[scriptName];
    if (typeof current !== 'string') return;
    if (current.includes('--webpack')) return;
    if (!/\bnext\s+(dev|build)\b/.test(current)) return;
    const updated = current.replace(
      /\bnext\s+(dev|build)\b/,
      'next $1 --webpack'
    );
    pkg.scripts[scriptName] = updated;
    changes.push(`scripts.${scriptName}: "${current}" -> "${updated}"`);
  });

  return changes;
}

function stepBump(cwd, flags) {
  if (flags.skipBump) {
    logStep('[3/10] Bump deps', 'skipped');
    return { hasSentry: false, changes: [] };
  }

  const pkg = readPackageJson(cwd);
  const hasSentry = Boolean(pkg.dependencies?.['@sentry/nextjs']);

  const allChanges = [];
  const deps = bumpDepsSection(pkg.dependencies, TARGET_MATRIX.dependencies);
  allChanges.push(...deps.changes);
  pkg.dependencies = deps.section;

  const devDeps = bumpDepsSection(
    pkg.devDependencies,
    TARGET_MATRIX.devDependencies
  );
  allChanges.push(...devDeps.changes);
  pkg.devDependencies = devDeps.section;

  if (hasSentry) {
    const sentryDeps = bumpDepsSection(pkg.dependencies, {
      '@sentry/nextjs': TARGET_MATRIX.conditional['@sentry/nextjs']
    });
    allChanges.push(...sentryDeps.changes);
    pkg.dependencies = sentryDeps.section;
  }

  const dropChanges = dropDeps(pkg, DEP_DROP);
  allChanges.push(...dropChanges);

  const scriptChanges = pinNextScriptsToWebpack(pkg);
  allChanges.push(...scriptChanges);

  if (allChanges.length === 0) {
    logStep('[3/10] Bump deps', 'already at target, no changes');
    return { hasSentry, changes: [] };
  }

  if (flags.dryRun) {
    logStep('[3/10] Bump deps', `[DRY] ${allChanges.length} changes`);
    allChanges.forEach((c) => log(`  - ${c}`));
  } else {
    writePackageJson(cwd, pkg);
    logStep('[3/10] Bump deps', `${allChanges.length} changes applied`);
    if (flags.verbose) allChanges.forEach((c) => log(`  - ${c}`));
  }

  return { hasSentry, changes: allChanges };
}

function stepNextCodemod(cwd, flags) {
  if (flags.skipNextCodemod) {
    logStep('[5/10] Next codemod', 'skipped');
    return { ok: true };
  }
  const cmd = flags.dryRun
    ? 'npx --yes @next/codemod@latest upgrade latest --verbose'
    : 'npx --yes @next/codemod@latest upgrade latest';
  logStep('[5/10] Next codemod', `running ${cmd}`);
  const result = runCommand(cmd, { cwd, dryRun: false, verbose: flags.verbose });
  if (result.status !== 0) {
    logStep('[5/10] Next codemod', `FAILED (exit ${result.status})`);
    return { ok: false };
  }
  logStep('[5/10] Next codemod', 'ok');
  return { ok: true };
}

function stepAuthCodemod(cwd, flags) {
  if (flags.skipAuth) {
    logStep('[6/10] Auth codemod', 'skipped');
    return { ok: true };
  }
  logStep('[6/10] Auth codemod', 'running migrate-auth-v5');
  try {
    const prevArgv = process.argv;
    process.argv = [
      process.argv[0],
      process.argv[1],
      ...(flags.dryRun ? ['--dry-run'] : [])
    ];
    const codemod = require(AUTH_CODEMOD);
    const savedCwd = process.cwd();
    process.chdir(cwd);
    const result = codemod.transform();
    const finish = () => {
      process.chdir(savedCwd);
      process.argv = prevArgv;
    };
    if (result && typeof result.then === 'function') {
      return result.then(
        () => {
          finish();
          logStep('[6/10] Auth codemod', 'ok');
          return { ok: true };
        },
        (err) => {
          finish();
          logStep('[6/10] Auth codemod', `FAILED: ${err.message}`);
          return { ok: false };
        }
      );
    }
    finish();
    logStep('[6/10] Auth codemod', 'ok');
    return { ok: true };
  } catch (err) {
    logStep('[6/10] Auth codemod', `FAILED: ${err.message}`);
    return { ok: false };
  }
}

function stepSentryCodemod(cwd, flags, hasSentry) {
  if (flags.skipSentry || !hasSentry) {
    const reason = flags.skipSentry ? 'skipped' : 'no @sentry/nextjs, skipping';
    logStep('[7/10] Sentry codemod', reason);
    return { ok: true };
  }
  if (flags.dryRun) {
    logStep('[7/10] Sentry codemod', '[DRY] would run sentry-9');
    return { ok: true };
  }
  logStep('[7/10] Sentry codemod', 'running sentry-9');
  try {
    const savedCwd = process.cwd();
    process.chdir(cwd);
    const codemod = require(SENTRY_CODEMOD);
    codemod.transform();
    process.chdir(savedCwd);
    logStep('[7/10] Sentry codemod', 'ok');
    return { ok: true };
  } catch (err) {
    logStep('[7/10] Sentry codemod', `FAILED: ${err.message}`);
    return { ok: false };
  }
}

function stepEslintCodemod(cwd, flags) {
  if (flags.skipEslint) {
    logStep('[8/10] ESLint codemod', 'skipped');
    return { ok: true };
  }
  logStep('[8/10] ESLint codemod', 'running migrate-eslint');
  try {
    const prevArgv = process.argv;
    process.argv = [
      process.argv[0],
      process.argv[1],
      ...(flags.dryRun ? ['--dry-run'] : [])
    ];
    const savedCwd = process.cwd();
    process.chdir(cwd);
    const codemod = require(ESLINT_CODEMOD);
    codemod.transform();
    process.chdir(savedCwd);
    process.argv = prevArgv;
    logStep('[8/10] ESLint codemod', 'ok');
    return { ok: true };
  } catch (err) {
    logStep('[8/10] ESLint codemod', `FAILED: ${err.message}`);
    return { ok: false };
  }
}

function stepInstall(cwd, flags) {
  if (flags.skipInstall) {
    logStep('[4/10] yarn install', 'skipped');
    return { ok: true };
  }
  if (flags.dryRun) {
    logStep('[4/10] yarn install', '[DRY] yarn clean && yarn');
    return { ok: true };
  }
  logStep('[4/10] yarn install', 'running yarn clean && yarn');
  const clean = runCommand('yarn clean', {
    cwd,
    dryRun: false,
    verbose: flags.verbose
  });
  if (clean.status !== 0) {
    log('  yarn clean failed, continuing with yarn install');
  }
  const install = runCommand('yarn', {
    cwd,
    dryRun: false,
    verbose: flags.verbose
  });
  if (install.status !== 0) {
    logStep('[4/10] yarn install', `FAILED (exit ${install.status})`);
    return { ok: false };
  }
  logStep('[4/10] yarn install', 'ok');
  return { ok: true };
}

function stepBuild(cwd, flags) {
  if (flags.skipBuild) {
    logStep('[9/10] yarn build', 'skipped');
    return { ok: true };
  }
  if (flags.dryRun) {
    logStep('[9/10] yarn build', '[DRY] yarn build');
    return { ok: true };
  }
  logStep('[9/10] yarn build', 'running');
  const result = runCommand('yarn build', {
    cwd,
    dryRun: false,
    verbose: flags.verbose
  });
  if (result.status !== 0) {
    logStep('[9/10] yarn build', `FAILED (exit ${result.status})`);
    return { ok: false };
  }
  logStep('[9/10] yarn build', 'ok');
  return { ok: true };
}

function countTodoMarkers(cwd) {
  const authFile = path.join(cwd, 'src', 'auth.ts');
  if (!fs.existsSync(authFile)) return 0;
  const content = fs.readFileSync(authFile, 'utf-8');
  return (content.match(/TODO:v5/g) || []).length;
}

function stepReport(cwd, results) {
  logStep('[10/10] Report', 'summary');
  const pkg = readPackageJson(cwd) || {};
  const todoCount = countTodoMarkers(cwd);

  console.log('\n==================== upgrade-to-2 summary ====================');
  console.log(`  Preflight:       ${results.preflight ? 'ok' : 'failed'}`);
  console.log(`  Backup branch:   ${results.backupBranch || 'skipped'}`);
  console.log(`  Deps changes:    ${results.bumpChanges}`);
  console.log(`  Install:         ${results.install ? 'ok' : 'FAILED'}`);
  console.log(`  Next codemod:    ${results.nextCodemod ? 'ok' : 'FAILED'}`);
  console.log(`  Auth codemod:    ${results.authCodemod ? 'ok' : 'FAILED'}`);
  console.log(`  Sentry codemod:  ${results.sentryCodemod ? 'ok' : results.sentrySkippedReason || 'FAILED'}`);
  console.log(`  ESLint codemod:  ${results.eslintCodemod ? 'ok' : 'FAILED'}`);
  console.log(`  Build:           ${results.build ? 'ok' : 'FAILED'}`);
  console.log('');
  console.log(`  Versions after:`);
  console.log(`    next:         ${pkg.dependencies?.next || '-'}`);
  console.log(`    react:        ${pkg.dependencies?.react || '-'}`);
  console.log(`    next-auth:    ${pkg.dependencies?.['next-auth'] || '-'}`);
  console.log(`    @akinon/next: ${pkg.dependencies?.['@akinon/next'] || '-'}`);
  if (pkg.dependencies?.['@sentry/nextjs']) {
    console.log(`    @sentry/nextjs: ${pkg.dependencies['@sentry/nextjs']}`);
  }
  console.log('');
  if (todoCount > 0) {
    console.log(`  Manual work left in src/auth.ts: ${todoCount} TODO:v5 markers`);
    console.log(`    grep -n "TODO:v5" src/auth.ts`);
  }
  console.log('  Next steps (manual):');
  console.log('    - Tailwind v4 migration: npx @tailwindcss/upgrade@latest');
  console.log('    - Review backup branch: ' + (results.backupBranch || 'n/a'));
  console.log('    - Run dev server and smoke test auth, cart, checkout');
  console.log('==============================================================\n');
}

async function maybePause(flags, stepLabel) {
  if (!flags.interactive) return true;
  const ok = await confirm(`Continue after ${stepLabel}?`);
  if (!ok) {
    log('Aborted by user.');
    process.exit(1);
  }
  return true;
}

async function transform() {
  const cwd = path.resolve(process.cwd());
  const flags = parseArgs(process.argv);

  log(`upgrade-to-2 starting in ${cwd}${flags.dryRun ? ' (DRY RUN)' : ''}`);

  const results = {};

  try {
    if (!flags.skipPreflight) {
      stepPreflight(cwd, flags);
      results.preflight = true;
      await maybePause(flags, 'preflight');
    } else {
      logStep('[1/10] Preflight', 'skipped');
      results.preflight = true;
    }

    results.backupBranch = stepBackup(cwd, flags);
    await maybePause(flags, 'backup');

    const bumpResult = stepBump(cwd, flags);
    results.bumpChanges = bumpResult.changes.length;
    const hasSentry = bumpResult.hasSentry;
    await maybePause(flags, 'bump');

    results.install = stepInstall(cwd, flags).ok;
    await maybePause(flags, 'install');

    results.nextCodemod = stepNextCodemod(cwd, flags).ok;
    await maybePause(flags, 'next-codemod');

    const authResult = stepAuthCodemod(cwd, flags);
    const authAwaited = authResult && typeof authResult.then === 'function'
      ? await authResult
      : authResult;
    results.authCodemod = authAwaited.ok;
    await maybePause(flags, 'auth-codemod');

    const sentryResult = stepSentryCodemod(cwd, flags, hasSentry);
    results.sentryCodemod = sentryResult.ok;
    if (flags.skipSentry) results.sentrySkippedReason = 'skipped';
    else if (!hasSentry) results.sentrySkippedReason = 'no @sentry/nextjs';
    await maybePause(flags, 'sentry-codemod');

    results.eslintCodemod = stepEslintCodemod(cwd, flags).ok;
    await maybePause(flags, 'eslint-codemod');

    results.build = stepBuild(cwd, flags).ok;
  } catch (err) {
    log(`FATAL: ${err.message}`);
    process.exitCode = 1;
  }

  stepReport(cwd, results);
  log('upgrade-to-2 done');
}

module.exports = { transform };

if (require.main === module) {
  transform().catch((err) => {
    console.error(err);
    process.exit(1);
  });
}
