/**
 * next-auth v4 -> v5 migration codemod for Project Zero brands.
 *
 * Handles two brand patterns:
 *   Tip A (re-export only): brand's [...nextauth].ts is a 3-line
 *     `import Auth from '@akinon/next/api/auth'; export default Auth;`
 *     → fully automated; brand's auth.ts + route.ts are generated.
 *
 *   Tip B (custom nextAuthOptions): brand has its own config using
 *     Pages API req/res. Skeleton is generated and original logic is
 *     transferred with TODO markers. Manual conversion of req/res to
 *     cookies()/headers() is required afterwards.
 *
 * In both cases, client usage `getServerSession` is rewritten to `auth`
 * via the sibling `transform.js` jscodeshift script.
 *
 * Usage:
 *   cd <brand-root>
 *   node <pz-next>/packages/projectzero/codemods/migrate-auth-v5/index.js [--dry-run]
 *
 * Or via the projectzero CLI:
 *   npx @akinon/projectzero codemod --codemod=migrate-auth-v5
 */

const path = require('path');
const fs = require('fs');
const jscodeshift = require('jscodeshift/src/Runner');

const TIP_A_SIZE_LIMIT = 256;

const TIP_A_AUTH_CONTENT = `import { createAuth } from '@akinon/next/api/auth';

export const { handlers, auth, signIn, signOut } = createAuth();
`;

const ROUTE_CONTENT = `import { handlers } from 'auth';

export const { GET, POST } = handlers;
`;

const TIP_B_HEADER = `/**
 * NOTE: This file was generated by \`migrate-auth-v5\`.
 * It replaces \`src/pages/api/auth/[...nextauth].ts\` which used
 * the Pages API \`req\`/\`res\` objects that next-auth v5 (App Router)
 * no longer provides.
 *
 * MANUAL MIGRATION STEPS (required before first run):
 *
 *   1. \`req.cookies['X']\`
 *        -> \`(await cookies()).get('X')?.value\`
 *
 *   2. \`req.headers['X']\`  or  \`req.headers.X\`
 *        -> \`(await headers()).get('X') ?? ''\`
 *
 *   3. \`res.setHeader('Set-Cookie', ['name=value; Path=/; HttpOnly; ...'])\`
 *        -> \`(await cookies()).set('name', value, { path: '/', httpOnly: true, secure: true, maxAge })\`
 *
 *   4. \`throw new Error(JSON.stringify(errors))\`
 *        -> \`class PzCredentialsError extends CredentialsSignin { constructor(errs) { super(); this.code = JSON.stringify(errs); } }\`
 *        -> \`throw new PzCredentialsError(errors)\`
 *        (Import \`CredentialsSignin\` from \`'next-auth'\`.)
 *
 *   5. Each \`authorize\` callback is now \`async\` and no longer receives
 *      the outer \`req\`/\`res\`. Wrap cookie/header reads in \`await\`.
 *
 * Reference implementation (copy the shape as needed):
 *   packages/akinon-next/api/auth.ts on the pz-next beta branch.
 *
 * Search for \`TODO:v5\` markers below for spots that definitely need work.
 */`;

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

function detectAuthType(cwd) {
  const oldPath = path.join(cwd, 'src', 'pages', 'api', 'auth', '[...nextauth].ts');
  if (!fs.existsSync(oldPath)) {
    return { type: null, oldPath };
  }

  const stats = fs.statSync(oldPath);
  const content = fs.readFileSync(oldPath, 'utf-8');

  const isReExport =
    stats.size < TIP_A_SIZE_LIMIT &&
    /from\s+['"]@akinon\/next\/api\/auth['"]/.test(content) &&
    /export\s+default\s+Auth/.test(content);

  return { type: isReExport ? 'A' : 'B', oldPath, content };
}

function ensureParentDir(filePath, { dryRun }) {
  const dir = path.dirname(filePath);
  if (!fs.existsSync(dir)) {
    if (dryRun) {
      log(`[DRY] mkdir -p ${dir}`);
    } else {
      fs.mkdirSync(dir, { recursive: true });
    }
  }
}

function writeFileSafe(filePath, content, { dryRun }) {
  ensureParentDir(filePath, { dryRun });
  if (dryRun) {
    log(`[DRY] CREATE ${filePath} (${content.length} bytes)`);
    return;
  }
  if (fs.existsSync(filePath)) {
    log(`  WARN: ${filePath} already exists; overwriting`);
  }
  fs.writeFileSync(filePath, content, 'utf-8');
  log(`  CREATE ${filePath}`);
}

function deleteFileSafe(filePath, { dryRun }) {
  if (!fs.existsSync(filePath)) return;
  if (dryRun) {
    log(`[DRY] DELETE ${filePath}`);
    return;
  }
  fs.unlinkSync(filePath);
  log(`  DELETE ${filePath}`);
}

function cleanupEmptyAuthPagesDir(cwd, { dryRun }) {
  const authDir = path.join(cwd, 'src', 'pages', 'api', 'auth');
  if (!fs.existsSync(authDir)) return;
  const remaining = fs.readdirSync(authDir);
  if (remaining.length > 0) return;
  if (dryRun) {
    log(`[DRY] rmdir ${authDir}`);
    return;
  }
  fs.rmdirSync(authDir);
  log(`  RMDIR ${authDir}`);
}

function migrateTipA(cwd, { dryRun, oldPath }) {
  const authPath = path.join(cwd, 'src', 'auth.ts');
  const routePath = path.join(
    cwd,
    'src',
    'app',
    'api',
    'auth',
    '[...nextauth]',
    'route.ts'
  );

  log(`Tip A migration`);
  deleteFileSafe(oldPath, { dryRun });
  writeFileSafe(authPath, TIP_A_AUTH_CONTENT, { dryRun });
  writeFileSafe(routePath, ROUTE_CONTENT, { dryRun });
  cleanupEmptyAuthPagesDir(cwd, { dryRun });
}

function transformTipBSource(original) {
  let src = original;

  src = src.replace(
    /import\s*\{\s*NextApiRequest\s*,\s*NextApiResponse\s*\}\s*from\s*['"]next['"];?\s*\n/,
    ''
  );

  src = src.replace(
    /import\s+NextAuth(?:\s*,\s*\{([^}]*)\})?\s+from\s+['"]next-auth['"];?\s*\n/,
    (_match, typeImports) => {
      const lines = [
        `import { createAuth } from '@akinon/next/api/auth';`,
        `import { cookies, headers } from 'next/headers';`
      ];
      if (typeImports && typeImports.trim()) {
        const cleaned = typeImports
          .split(',')
          .map((s) => s.trim())
          .filter(Boolean)
          .join(', ');
        if (cleaned) {
          lines.push(`import type { ${cleaned} } from 'next-auth';`);
        }
      }
      return lines.join('\n') + '\n';
    }
  );

  src = src.replace(
    /const\s+nextAuthOptions\s*=\s*\(\s*req\s*:\s*NextApiRequest\s*,\s*res\s*:\s*NextApiResponse\s*\)\s*=>\s*\{/,
    `const getCustomAuthConfig = () => {\n  // TODO:v5 See migration notes at top of file.`
  );

  src = src.replace(
    /const\s+nextAuthOptions\s*=\s*\(\s*req\s*,\s*res\s*\)\s*=>\s*\{/,
    `const getCustomAuthConfig = () => {\n  // TODO:v5 See migration notes at top of file.`
  );

  src = src.replace(
    /const\s+Auth\s*=\s*\(\s*req[^)]*\)\s*=>\s*\{\s*return\s+NextAuth\(\s*req\s*,\s*res\s*,\s*nextAuthOptions\(\s*req\s*,\s*res\s*\)\s*\);\s*\};?\s*\n/,
    ''
  );

  src = src.replace(
    /const\s+Auth\s*=\s*\(\s*req[^)]*\)\s*=>\s*NextAuth\([^;]*\);\s*\n/,
    ''
  );

  src = src.replace(
    /export\s+default\s+Auth\s*;?\s*$/m,
    `export const { handlers, auth, signIn, signOut } = createAuth(getCustomAuthConfig);\n`
  );

  src = src.replace(
    /\b(req\.cookies(?:\?\.|\.)[a-zA-Z_$][\w$]*|req\.cookies\[[^\]]+\])/g,
    '/* TODO:v5 cookies() */ $1'
  );
  src = src.replace(
    /\b(req\.headers(?:\?\.|\.)[a-zA-Z_$][\w$]*|req\.headers\[[^\]]+\])/g,
    '/* TODO:v5 headers() */ $1'
  );
  src = src.replace(
    /\b(req\.body(?:\?\.|\.)?[a-zA-Z_$]?[\w$]*)/g,
    '/* TODO:v5 read body via credentials */ $1'
  );
  src = src.replace(
    /\b(res\.[a-zA-Z_$][\w$]*\()/g,
    '/* TODO:v5 cookies().set */ $1'
  );
  src = src.replace(
    /\bthrow\s+new\s+Error\(JSON\.stringify\(errors\)\)/g,
    '/* TODO:v5 CredentialsSignin */ throw new Error(JSON.stringify(errors))'
  );

  return `${TIP_B_HEADER}\n\n${src}`;
}

function migrateTipB(cwd, { dryRun, oldPath, content }) {
  const authPath = path.join(cwd, 'src', 'auth.ts');
  const routePath = path.join(
    cwd,
    'src',
    'app',
    'api',
    'auth',
    '[...nextauth]',
    'route.ts'
  );

  log(`Tip B migration (${content.length} bytes to transfer)`);

  const newAuthContent = transformTipBSource(content);

  writeFileSafe(authPath, newAuthContent, { dryRun });
  writeFileSafe(routePath, ROUTE_CONTENT, { dryRun });
  deleteFileSafe(oldPath, { dryRun });
  cleanupEmptyAuthPagesDir(cwd, { dryRun });

  log(`  Manual work left: req/res -> cookies()/headers() conversions inside ${authPath}`);
  log(`  Grep for 'TODO:v5' to find spots.`);
}

function runClientTransform(cwd, { dryRun }) {
  const transformPath = path.resolve(__dirname, 'transform.js');
  const targets = ['src'].map((d) => path.join(cwd, d)).filter(fs.existsSync);
  if (targets.length === 0) {
    log(`No src/ directory found, skipping client transform`);
    return Promise.resolve();
  }

  log(`Running client transform (getServerSession -> auth)`);

  return jscodeshift
    .run(transformPath, targets, {
      verbose: 0,
      dry: Boolean(dryRun),
      print: Boolean(dryRun),
      extensions: 'ts,tsx',
      parser: 'tsx',
      ignorePattern: '**/node_modules/**',
      silent: true
    })
    .then(
      (stats) => {
        log(
          `  Client transform: ${stats.ok} changed, ${stats.nochange} unchanged, ${stats.error} errored`
        );
      },
      (err) => {
        log(`  Client transform error: ${err.message}`);
      }
    );
}

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

  log(
    `migrate-auth-v5 starting in ${workingDir}${dryRun ? ' (DRY RUN)' : ''}`
  );

  const detection = detectAuthType(workingDir);

  if (!detection.type) {
    log(`No src/pages/api/auth/[...nextauth].ts found, nothing to migrate.`);
    return Promise.resolve();
  }

  log(`Detected pattern: Tip ${detection.type}`);

  if (detection.type === 'A') {
    migrateTipA(workingDir, { dryRun, oldPath: detection.oldPath });
  } else {
    migrateTipB(workingDir, {
      dryRun,
      oldPath: detection.oldPath,
      content: detection.content
    });
  }

  return runClientTransform(workingDir, { dryRun }).then(() => {
    log(`migrate-auth-v5 done`);
  });
};

module.exports = {
  transform
};

if (require.main === module) {
  const result = transform();
  if (result && typeof result.then === 'function') {
    result.catch((err) => {
      console.error(err);
      process.exit(1);
    });
  }
}
