/**
 * ESLint flat config migration codemod for Project Zero brands.
 *
 * Converts a brand project from legacy `.eslintrc.js` to ESLint v9 flat config
 * (`eslint.config.mjs`), consuming the shared base from
 * `@akinon/next/eslint.config.base.mjs`.
 *
 * Idempotent: re-running on an already migrated project is a no-op for
 * already-applied steps.
 *
 * Steps:
 *   1. Read .eslintrc.js (if present) to detect brand-specific settings
 *      (settings.next.rootDir, custom rules).
 *   2. Read .eslintignore (if present) to collect ignore patterns.
 *   3. Write eslint.config.mjs (skipped if already exists).
 *   4. Delete .eslintrc.js and .eslintignore.
 *   5. Rewrite .lintstagedrc.js to use `eslint --fix` instead of `next lint`.
 *   6. Patch package.json:
 *      - scripts.lint: "next lint" -> "eslint ."
 *      - eslint: ^8.x -> ^9.39.4
 *      - eslint-config-prettier: ^8.x -> ^10.1.1
 *      - remove @typescript-eslint/eslint-plugin and @typescript-eslint/parser
 *        (now provided by eslint-config-next/typescript).
 *
 * Usage:
 *   cd <brand-root>
 *   npx @akinon/projectzero codemod --codemod=migrate-eslint
 */

'use strict';

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

const ESLINT_TARGET = '9.39.4';
const ESLINT_CONFIG_PRETTIER_TARGET = '10.1.1';
const TYPESCRIPT_ESLINT_DROP = [
  '@typescript-eslint/eslint-plugin',
  '@typescript-eslint/parser'
];

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

function buildFlatConfig({ rootDir, ignores, customRules }) {
  const lines = [
    "import { defineConfig, globalIgnores } from 'eslint/config';",
    "import baseConfig from '@akinon/next/eslint.config.base.mjs';",
    '',
    'export default defineConfig([',
    '  ...baseConfig'
  ];

  if (rootDir) {
    lines[lines.length - 1] += ',';
    lines.push(
      '  {',
      '    settings: {',
      '      next: {',
      `        rootDir: ${JSON.stringify(rootDir)}`,
      '      }',
      '    }',
      '  }'
    );
  }

  if (customRules) {
    lines[lines.length - 1] += ',';
    const indented = JSON.stringify(customRules, null, 2)
      .split('\n')
      .map((l, i) => (i === 0 ? '    rules: ' + l : '    ' + l))
      .join('\n');
    lines.push('  {', indented, '  }');
  }

  if (ignores && ignores.length) {
    lines[lines.length - 1] += ',';
    lines.push(`  globalIgnores(${JSON.stringify(ignores)})`);
  }

  lines.push(']);', '');

  return lines.join('\n');
}

const LINT_STAGED_TEMPLATE = `const path = require('path');

const buildEslintCommand = (filenames) =>
  \`eslint --fix \${filenames
    .map((f) => \`"\${path.relative(process.cwd(), f)}"\`)
    .join(' ')}\`;

module.exports = {
  '**/*.(ts|tsx)': () => 'yarn tsc --noEmit',
  '*.{js,jsx,ts,tsx}': [buildEslintCommand, 'prettier --write'],
  '*.{css,scss}': [
    'stylelint --fix --allow-empty-input',
    'prettier --write',
    'stylelint --allow-empty-input'
  ]
};
`;

function loadLegacyConfig(eslintrcPath) {
  try {
    delete require.cache[require.resolve(eslintrcPath)];
    return require(eslintrcPath);
  } catch (err) {
    return { __parseError: err.message };
  }
}

function readIgnoreFile(eslintignorePath) {
  if (!fs.existsSync(eslintignorePath)) return [];
  const raw = fs.readFileSync(eslintignorePath, 'utf8');
  return raw
    .split(/\r?\n/)
    .map((line) => line.trim())
    .filter((line) => line && !line.startsWith('#'))
    .map((pattern) => (pattern.endsWith('/**') ? pattern : pattern + '/**'));
}

function patchPackageJson(pkgPath, { dryRun }) {
  const raw = fs.readFileSync(pkgPath, 'utf8');
  const pkg = JSON.parse(raw);
  const changes = [];

  if (pkg.scripts && typeof pkg.scripts.lint === 'string') {
    if (/^next\s+lint\b/.test(pkg.scripts.lint.trim())) {
      pkg.scripts.lint = 'eslint .';
      changes.push('scripts.lint -> "eslint ."');
    }
  }

  const bumpDev = (name, target) => {
    if (!pkg.devDependencies || !pkg.devDependencies[name]) return;
    const current = pkg.devDependencies[name].replace(/^\^|^~/, '');
    if (current === target) return;
    pkg.devDependencies[name] = target;
    changes.push(`devDependencies.${name} ${current} -> ${target}`);
  };

  bumpDev('eslint', ESLINT_TARGET);
  bumpDev('eslint-config-prettier', ESLINT_CONFIG_PRETTIER_TARGET);

  for (const dep of TYPESCRIPT_ESLINT_DROP) {
    if (pkg.devDependencies && pkg.devDependencies[dep]) {
      delete pkg.devDependencies[dep];
      changes.push(`removed devDependencies.${dep}`);
    }
  }

  if (changes.length === 0) return { changed: false, changes };

  if (!dryRun) {
    fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
  }
  return { changed: true, changes };
}

const transform = () => {
  const root = path.resolve(process.cwd());
  const dryRun = process.argv.includes('--dry-run');

  const eslintrcPath = path.join(root, '.eslintrc.js');
  const eslintignorePath = path.join(root, '.eslintignore');
  const flatConfigPath = path.join(root, 'eslint.config.mjs');
  const lintStagedPath = path.join(root, '.lintstagedrc.js');
  const packageJsonPath = path.join(root, 'package.json');

  if (!fs.existsSync(packageJsonPath)) {
    log(`No package.json found in ${root}`);
    process.exitCode = 1;
    return;
  }

  log(`starting in ${root}${dryRun ? ' (DRY RUN)' : ''}`);
  const summary = [];

  const legacyConfig = fs.existsSync(eslintrcPath)
    ? loadLegacyConfig(eslintrcPath)
    : null;

  if (legacyConfig && legacyConfig.__parseError) {
    log(
      `WARN: could not parse .eslintrc.js (${legacyConfig.__parseError}); continuing with default brand template`
    );
  }

  const rootDir = legacyConfig?.settings?.next?.rootDir;
  const customRules =
    legacyConfig && legacyConfig.rules && Object.keys(legacyConfig.rules).length
      ? legacyConfig.rules
      : null;

  let ignores = readIgnoreFile(eslintignorePath);
  if (ignores.length === 0) ignores = ['public/**'];

  if (!fs.existsSync(flatConfigPath)) {
    const body = buildFlatConfig({ rootDir, ignores, customRules });
    if (dryRun) {
      log(`[DRY] CREATE ${flatConfigPath} (${body.length} bytes)`);
    } else {
      fs.writeFileSync(flatConfigPath, body, 'utf8');
    }
    summary.push('created eslint.config.mjs');
  } else {
    summary.push('eslint.config.mjs already exists (skipped)');
  }

  if (fs.existsSync(eslintrcPath)) {
    if (dryRun) {
      log(`[DRY] DELETE ${eslintrcPath}`);
    } else {
      fs.unlinkSync(eslintrcPath);
    }
    summary.push('removed .eslintrc.js');
  }

  if (fs.existsSync(eslintignorePath)) {
    if (dryRun) {
      log(`[DRY] DELETE ${eslintignorePath}`);
    } else {
      fs.unlinkSync(eslintignorePath);
    }
    summary.push('removed .eslintignore');
  }

  if (fs.existsSync(lintStagedPath)) {
    const before = fs.readFileSync(lintStagedPath, 'utf8');
    if (before !== LINT_STAGED_TEMPLATE) {
      if (dryRun) {
        log(`[DRY] REWRITE ${lintStagedPath}`);
      } else {
        fs.writeFileSync(lintStagedPath, LINT_STAGED_TEMPLATE, 'utf8');
      }
      summary.push('rewrote .lintstagedrc.js');
    } else {
      summary.push('.lintstagedrc.js already up to date (skipped)');
    }
  }

  const pkgResult = patchPackageJson(packageJsonPath, { dryRun });
  if (pkgResult.changed) {
    summary.push(`patched package.json (${pkgResult.changes.join(', ')})`);
  } else {
    summary.push('package.json already up to date (skipped)');
  }

  console.log('\nESLint flat config migration summary:');
  for (const line of summary) console.log(`  - ${line}`);

  if (customRules) {
    console.log(
      `\nNote: ${Object.keys(customRules).length} custom rule(s) from the old ` +
        '.eslintrc.js were carried over into eslint.config.mjs.\nReview if ' +
        'they are still needed (some may already be covered by the base config).'
    );
  }

  log('done');
};

module.exports = { transform };

if (require.main === module) {
  transform();
}
