#!/usr/bin/env node
/**
 * Replace <a href="/internal"> with <Link href="/internal"> in JSX.
 * Skips <a> tags that already opt out of navigation (target, download,
 * rel, mailto, tel, external, hash-only) and any non-string-literal
 * href expressions.
 *
 * Idempotent: re-running on an already-converted file is a no-op.
 *
 * Mirrors the react-doctor rule
 * `react-doctor/nextjs-no-a-element`.
 */
const fs = require("node:fs");
const path = require("node:path");
const ts = require("typescript");

function findInternalAnchorOpeningElements(src) {
  const sf = ts.createSourceFile("x.tsx", src, ts.ScriptTarget.Latest, true);
  /** @type {Array<{pos:number,end:number,href:string}>} */
  const matches = [];
  function visit(node) {
    if (
      ts.isJsxOpeningElement(node) &&
      ts.isIdentifier(node.tagName) &&
      node.tagName.text === "a"
    ) {
      let hrefValue = null;
      let isInternalLiteral = false;
      let isOptOut = false;
      for (const attr of node.attributes.properties) {
        if (!ts.isJsxAttribute(attr) || !attr.name) continue;
        const name = ts.isIdentifier(attr.name) ? attr.name.text : attr.name.text;
        if (name === "href" && attr.initializer && ts.isStringLiteral(attr.initializer)) {
          hrefValue = attr.initializer.text;
          if (hrefValue.startsWith("/")) {
            isInternalLiteral = true;
          }
        }
        if (name === "target" || name === "download" || name === "rel" || name === "onClick") {
          isOptOut = true;
        }
      }
      if (isInternalLiteral && !isOptOut) {
        matches.push({
          pos: node.getStart(sf),
          end: node.getEnd(),
          href: hrefValue,
        });
      }
    }
    ts.forEachChild(node, visit);
  }
  visit(sf);
  return matches;
}

function ensureLinkImport(src) {
  if (/from\s+["']next\/link["']/.test(src)) return src;
  // Insert after the last existing import line.
  const sf = ts.createSourceFile("x.tsx", src, ts.ScriptTarget.Latest, true);
  let lastImportEnd = 0;
  for (const stmt of sf.statements) {
    if (ts.isImportDeclaration(stmt)) {
      lastImportEnd = Math.max(lastImportEnd, stmt.getEnd());
    }
  }
  if (lastImportEnd === 0) {
    return `import Link from "next/link";\n${src}`;
  }
  return `${src.slice(0, lastImportEnd)}\nimport Link from "next/link";${src.slice(lastImportEnd)}`;
}

function processFile(filePath) {
  const abs = path.resolve(filePath);
  if (!fs.existsSync(abs)) {
    console.error(`skip (not found): ${abs}`);
    return 0;
  }
  const original = fs.readFileSync(abs, "utf8");
  const matches = findInternalAnchorOpeningElements(original);
  if (matches.length === 0) return 0;

  let out = original;
  // Apply edits in reverse so positions don't shift.
  matches.sort((a, b) => b.pos - a.pos);
  for (const m of matches) {
    out = out.slice(0, m.pos) + `<Link href="${m.href}"` + out.slice(m.end);
  }
  // Replace any closing </a> with </Link> in the same file. Safer
  // to do this as a single global swap after the opening-tag edits
  // since the file is the scope.
  out = out.replace(/<\/a>/g, "</Link>");

  out = ensureLinkImport(out);
  if (out !== original) {
    fs.writeFileSync(abs, out, "utf8");
    console.log(`patched (${matches.length} anchors): ${abs}`);
    return 1;
  }
  return 0;
}

const files = process.argv.slice(2);
if (files.length === 0) {
  console.error("usage: node scripts/fix-next-anchor.js <file>...");
  process.exit(2);
}
let n = 0;
for (const f of files) {
  n += processFile(f);
}
console.log(`done — ${n} files patched`);
