/**
 * One-shot migration: wraps every internal API route in `src/app/api/`
 * with the `withAuth()` HOF so each handler receives a typed UserContext.
 *
 * Run:   npx tsx scripts/migrate-routes-to-with-auth.ts
 * Dry:   npx tsx scripts/migrate-routes-to-with-auth.ts --dry
 *
 * Public routes (track/*, m365-diagnose, trigger/[key]) are skipped — they
 * authenticate via tracking-pixel logic, public-pinger, or per-template API
 * keys respectively.
 *
 * Idempotent: re-running on already-migrated files is a no-op.
 *
 * Implementation note: we tokenize each route file character-by-character,
 * skipping strings/comments, and brace-count *function bodies only* — this
 * avoids being misled by `{` braces inside type annotations like
 * `{ params: Promise<{ id: string }> }` in the signature.
 */

import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
import { join } from 'node:path';

const DRY = process.argv.includes('--dry');

const PUBLIC_PATH_PATTERNS = [
  /\/api\/track\//,
  /\/api\/m365-diagnose\//,
  /\/api\/trigger\/\[key\]\//,
];

function walkRouteFiles(dir: string, acc: string[] = []): string[] {
  for (const entry of readdirSync(dir)) {
    const full = join(dir, entry);
    const st = statSync(full);
    if (st.isDirectory()) walkRouteFiles(full, acc);
    else if (entry === 'route.ts') acc.push(full);
  }
  return acc;
}

function listRouteFiles(): string[] {
  return walkRouteFiles('src/app/api').filter(
    (p) => !PUBLIC_PATH_PATTERNS.some((rx) => rx.test('/' + p.replace(/\\/g, '/'))),
  );
}

const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const;
type HttpMethod = (typeof HTTP_METHODS)[number];

interface FunctionDecl {
  method: HttpMethod;
  /** Char offset of the `export` keyword. */
  exportStartOffset: number;
  /** Char offset of `(` that opens the parameter list. */
  parenOpenOffset: number;
  /** Char offset of `)` that closes the parameter list. */
  parenCloseOffset: number;
  /** Char offset of `{` that opens the function body. */
  braceOpenOffset: number;
  /** Char offset of matching `}` that closes the function body. */
  braceCloseOffset: number;
  /** Full text of the parameter list (between parens). */
  paramsText: string;
}

/**
 * Advance past a string literal starting at `i` (current char is opening
 * quote). Returns offset of the character AFTER the closing quote.
 */
function skipString(src: string, i: number): number {
  const quote = src[i];
  i++;
  while (i < src.length) {
    if (src[i] === '\\') {
      i += 2;
      continue;
    }
    if (src[i] === quote) return i + 1;
    if (quote === '`' && src[i] === '$' && src[i + 1] === '{') {
      // template-literal interpolation — find matching `}`
      i += 2;
      let depth = 1;
      while (i < src.length && depth > 0) {
        if (src[i] === '{') depth++;
        else if (src[i] === '}') depth--;
        i++;
      }
      continue;
    }
    i++;
  }
  return i;
}

/**
 * Advance past a line/block comment starting at `i` (src[i] === '/').
 * Returns offset of the character AFTER the comment, or `i` unchanged if not
 * actually a comment.
 */
function skipComment(src: string, i: number): number {
  if (src[i] !== '/') return i;
  if (src[i + 1] === '/') {
    const nl = src.indexOf('\n', i);
    return nl === -1 ? src.length : nl + 1;
  }
  if (src[i + 1] === '*') {
    const end = src.indexOf('*/', i + 2);
    return end === -1 ? src.length : end + 2;
  }
  return i;
}

/**
 * Brace-balanced match for the closing `)` corresponding to the `(` at `openOffset`.
 * String/comment-aware.
 */
function matchParen(src: string, openOffset: number): number {
  let depth = 0;
  for (let i = openOffset; i < src.length; i++) {
    const ch = src[i];
    if (ch === '"' || ch === "'" || ch === '`') {
      i = skipString(src, i) - 1;
      continue;
    }
    if (ch === '/' && (src[i + 1] === '/' || src[i + 1] === '*')) {
      i = skipComment(src, i) - 1;
      continue;
    }
    if (ch === '(') depth++;
    else if (ch === ')') {
      depth--;
      if (depth === 0) return i;
    }
  }
  return -1;
}

/**
 * Brace-balanced match for the closing `}` of a function body whose opening
 * `{` is at `openOffset`. String/comment-aware.
 */
function matchBrace(src: string, openOffset: number): number {
  let depth = 0;
  for (let i = openOffset; i < src.length; i++) {
    const ch = src[i];
    if (ch === '"' || ch === "'" || ch === '`') {
      i = skipString(src, i) - 1;
      continue;
    }
    if (ch === '/' && (src[i + 1] === '/' || src[i + 1] === '*')) {
      i = skipComment(src, i) - 1;
      continue;
    }
    if (ch === '{') depth++;
    else if (ch === '}') {
      depth--;
      if (depth === 0) return i;
    }
  }
  return -1;
}

const SIG_RX = new RegExp(
  String.raw`export\s+async\s+function\s+(${HTTP_METHODS.join('|')})\s*\(`,
  'g',
);

function findFunctions(src: string): FunctionDecl[] {
  const results: FunctionDecl[] = [];
  SIG_RX.lastIndex = 0;
  let m: RegExpExecArray | null;
  while ((m = SIG_RX.exec(src)) !== null) {
    const exportStartOffset = m.index;
    const parenOpenOffset = m.index + m[0].length - 1;
    const parenCloseOffset = matchParen(src, parenOpenOffset);
    if (parenCloseOffset === -1) continue;

    // Find the next `{` after the closing `)` — that's the function body.
    let braceOpenOffset = -1;
    for (let i = parenCloseOffset + 1; i < src.length; i++) {
      const ch = src[i];
      if (ch === '{') {
        braceOpenOffset = i;
        break;
      }
      // Tolerate whitespace and an optional return-type annotation like `: Promise<NextResponse>`.
      if (ch === ':' || ch === '\n' || ch === ' ' || ch === '\t') continue;
      // Type annotation chars — let them flow.
    }
    if (braceOpenOffset === -1) continue;
    const braceCloseOffset = matchBrace(src, braceOpenOffset);
    if (braceCloseOffset === -1) continue;

    results.push({
      method: m[1] as HttpMethod,
      exportStartOffset,
      parenOpenOffset,
      parenCloseOffset,
      braceOpenOffset,
      braceCloseOffset,
      paramsText: src.slice(parenOpenOffset + 1, parenCloseOffset),
    });
  }
  return results;
}

function splitTopLevelCommas(s: string): string[] {
  const out: string[] = [];
  let depth = 0;
  let buf = '';
  for (let i = 0; i < s.length; i++) {
    const ch = s[i];
    if (ch === '"' || ch === "'" || ch === '`') {
      const end = skipString(s, i);
      buf += s.slice(i, end);
      i = end - 1;
      continue;
    }
    if (ch === '<' || ch === '(' || ch === '{' || ch === '[') depth++;
    else if (ch === '>' || ch === ')' || ch === '}' || ch === ']') depth--;
    if (ch === ',' && depth === 0) {
      out.push(buf);
      buf = '';
      continue;
    }
    buf += ch;
  }
  if (buf.trim().length > 0) out.push(buf);
  return out;
}

function findTopLevelColon(s: string): number {
  let depth = 0;
  for (let i = 0; i < s.length; i++) {
    const ch = s[i];
    if (ch === '"' || ch === "'" || ch === '`') {
      i = skipString(s, i) - 1;
      continue;
    }
    if (ch === '<' || ch === '(' || ch === '{' || ch === '[') depth++;
    else if (ch === '>' || ch === ')' || ch === '}' || ch === ']') depth--;
    else if (ch === ':' && depth === 0) return i;
  }
  return -1;
}

interface Rewrite {
  /** Offset to start replacing. */
  from: number;
  /** Offset to stop replacing (exclusive). */
  to: number;
  /** Replacement text. */
  text: string;
}

function buildRewrites(src: string, decl: FunctionDecl): Rewrite[] {
  const params = splitTopLevelCommas(decl.paramsText);
  const secondArg = params.length >= 2 ? params.slice(1).join(',').trim() : null;

  let generic = '';
  let routeCtxParam = '';
  if (secondArg) {
    const colon = findTopLevelColon(secondArg);
    const type = colon !== -1 ? secondArg.slice(colon + 1).trim() : 'unknown';
    generic = `<${type}>`;
    routeCtxParam = `, ${secondArg}`;
  }

  // Replace the signature `export async function FOO(args) {` with
  // `export const FOO = withAuth<...>(async (req: NextRequest, _ctx[, routeCtx]) => {`
  const newSig = `export const ${decl.method} = withAuth${generic}(async (req: NextRequest, _ctx${routeCtxParam}) => {`;

  // The matching closing brace must become `});`
  return [
    { from: decl.exportStartOffset, to: decl.braceOpenOffset + 1, text: newSig },
    { from: decl.braceCloseOffset, to: decl.braceCloseOffset + 1, text: '});' },
  ];
}

function applyRewrites(src: string, rewrites: Rewrite[]): string {
  // Apply from end to start so offsets stay valid.
  const sorted = [...rewrites].sort((a, b) => b.from - a.from);
  let out = src;
  for (const r of sorted) {
    out = out.slice(0, r.from) + r.text + out.slice(r.to);
  }
  return out;
}

function insertImport(src: string): string {
  // Find the end of the import block. An import statement may span multiple
  // lines (e.g. `import {\n  a,\n  b,\n} from 'x';`). We walk through
  // top-level statements until we encounter the first non-import / non-comment
  // / non-blank statement, then insert our import just before it.
  const lines = src.split('\n');
  let i = 0;
  let lastImportEndLine = -1;

  while (i < lines.length) {
    const line = lines[i];
    const trimmed = line.trim();

    if (trimmed === '' || trimmed.startsWith('//') || trimmed.startsWith('/*')) {
      i++;
      continue;
    }
    if (trimmed.startsWith('*') || trimmed.startsWith('*/')) {
      i++;
      continue;
    }

    if (trimmed.startsWith('import')) {
      // Consume until the import's terminating `;` at top-level.
      // String/comment-aware brace counter on accumulated text from this line forward.
      let acc = '';
      let endLine = i;
      while (endLine < lines.length) {
        acc += (acc ? '\n' : '') + lines[endLine];
        if (importEndsHere(acc)) {
          lastImportEndLine = endLine;
          i = endLine + 1;
          break;
        }
        endLine++;
      }
      if (endLine >= lines.length) break;
      continue;
    }

    // First non-import top-level statement — stop here.
    break;
  }

  const insertAt = lastImportEndLine === -1 ? 0 : lastImportEndLine + 1;
  lines.splice(insertAt, 0, `import { withAuth } from '@/core/auth';`);
  return lines.join('\n');
}

/**
 * After migration each handler has a `req: NextRequest` parameter. If the
 * original file imported only `NextResponse`, add `NextRequest` to that import.
 * If no `next/server` import exists at all, prepend one.
 */
function ensureNextRequestImport(src: string): string {
  if (/\bNextRequest\b/.test(src.split('\n').filter((l) => /^import\s+/.test(l)).join('\n'))) {
    // Already imported somewhere — multi-line case is also covered by checking
    // the import-block as a whole below.
  }

  // Find any `import ... from 'next/server';` line and add NextRequest if missing.
  const importRx = /^(import\s*\{[^}]*\}\s*from\s*['"]next\/server['"];?)$/m;
  const m = src.match(importRx);
  if (m) {
    const original = m[1];
    if (/\bNextRequest\b/.test(original)) return src;
    // Insert NextRequest into the brace list.
    const replaced = original.replace(/\{\s*([^}]*?)\s*\}/, (_, inside: string) => {
      const items = inside
        .split(',')
        .map((s) => s.trim())
        .filter(Boolean);
      items.unshift('NextRequest');
      return `{ ${items.join(', ')} }`;
    });
    return src.replace(original, replaced);
  }

  // No next/server import at all — prepend one before the first existing import.
  const firstImport = src.indexOf('import ');
  if (firstImport === -1) {
    return `import { NextRequest } from 'next/server';\n${src}`;
  }
  return (
    src.slice(0, firstImport) +
    `import { NextRequest } from 'next/server';\n` +
    src.slice(firstImport)
  );
}

function importEndsHere(s: string): boolean {
  // True if the accumulated text constitutes a complete import statement
  // (ends with `;` at top-level outside strings/comments).
  let depth = 0;
  let lastNonSpace = '';
  for (let i = 0; i < s.length; i++) {
    const ch = s[i];
    if (ch === '"' || ch === "'" || ch === '`') {
      i = skipString(s, i) - 1;
      lastNonSpace = ch;
      continue;
    }
    if (ch === '/' && (s[i + 1] === '/' || s[i + 1] === '*')) {
      i = skipComment(s, i) - 1;
      continue;
    }
    if (ch === '{' || ch === '[' || ch === '(') depth++;
    else if (ch === '}' || ch === ']' || ch === ')') depth--;
    if (ch.trim()) lastNonSpace = ch;
  }
  return depth === 0 && lastNonSpace === ';';
}

function migrateOne(filePath: string): 'migrated' | 'skipped' | 'no-functions' {
  const src = readFileSync(filePath, 'utf8');
  if (src.includes(`from '@/core/auth'`) && src.includes('withAuth')) {
    return 'skipped';
  }

  const decls = findFunctions(src);
  if (decls.length === 0) return 'no-functions';

  const rewrites = decls.flatMap((d) => buildRewrites(src, d));
  let out = applyRewrites(src, rewrites);
  out = insertImport(out);
  out = ensureNextRequestImport(out);

  if (DRY) {
    console.log(`  [dry] would migrate ${filePath} (${decls.length} handlers)`);
  } else {
    writeFileSync(filePath, out);
    console.log(`  ✓ ${filePath} (${decls.length} handlers)`);
  }
  return 'migrated';
}

function run(): void {
  const files = listRouteFiles();
  console.log(`Migrating ${files.length} routes${DRY ? ' (dry run)' : ''}`);

  let migrated = 0;
  let skipped = 0;
  let empty = 0;
  for (const file of files) {
    const r = migrateOne(file);
    if (r === 'migrated') migrated++;
    else if (r === 'skipped') skipped++;
    else empty++;
  }

  console.log('---');
  console.log(`✓ Migrated: ${migrated}`);
  console.log(`  Skipped (already done): ${skipped}`);
  console.log(`  No HTTP handlers found: ${empty}`);
}

run();
