#!/usr/bin/env tsx
/**
 * Codegen: swift-fix-rules.ts → AxintFixer.swift
 *
 * The Xcode Source Editor Extension can't shell out to node, so it ships
 * a Swift-native fixer. The TypeScript rules registry is the source of
 * truth — this script reads it and emits a Swift file that applies the
 * same fixes via NSRegularExpression and a hand-written brace matcher.
 *
 * Run:  npm run gen:swift-fixer
 * CI:   `npm run gen:swift-fixer -- --check` fails if the file is stale.
 */

import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import {
  REGEX_FIX_RULES,
  STRUCT_INJECT_FIX_RULES,
  type RegexFix,
  type StructInjectFix,
} from "../src/core/swift-fix-rules.js";

const here = dirname(fileURLToPath(import.meta.url));
const TARGET = resolve(
  here,
  "../extensions/xcode/source-editor-extension/AxintEditor/AxintFixer.swift"
);

function render(): string {
  const header = `// AUTO-GENERATED by scripts/gen-swift-fixer.ts from src/core/swift-fix-rules.ts.
// Do not edit by hand — run 'npm run gen:swift-fixer' to regenerate.
//
// The Xcode Source Editor Extension is sandboxed and can't shell out to
// the axint CLI, so we ship a Swift-native implementation of the fix
// rules. Both this file and the TypeScript fixer read from the same
// registry, so they stay in lockstep.

import Foundation

struct AxintFix: Equatable {
    let code: String
    let message: String
}

struct AxintFixResult {
    let source: String
    let applied: [AxintFix]
}

enum AxintFixer {
    static func fix(_ source: String) -> AxintFixResult {
        var current = source
        var applied: [AxintFix] = []

${REGEX_FIX_RULES.map((r) => regexCall(r)).join("\n\n")}

${STRUCT_INJECT_FIX_RULES.map((r) => structCall(r)).join("\n\n")}

        return AxintFixResult(source: current, applied: applied)
    }
`;

  const regexFn = `
    // MARK: - Regex helper

    private static func applyRegex(
        _ source: String,
        pattern: String,
        template: String
    ) -> (String, Int) {
        guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
            return (source, 0)
        }
        let range = NSRange(source.startIndex..., in: source)
        let matches = regex.matches(in: source, options: [], range: range)
        if matches.isEmpty { return (source, 0) }
        let rewritten = regex.stringByReplacingMatches(
            in: source,
            options: [],
            range: range,
            withTemplate: template
        )
        return (rewritten, matches.count)
    }
`;

  const injectFn = `
    // MARK: - Struct body injection

    private static func injectIntoStruct(
        _ source: String,
        conformance: String,
        sentinel: String,
        stub: String
    ) -> String? {
        let escaped = NSRegularExpression.escapedPattern(for: conformance)
        let pattern = "\\\\bstruct\\\\s+(\\\\w+)\\\\s*:\\\\s*[^{]*\\\\b\\(escaped)\\\\b[^{]*\\\\{"
        guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
            return nil
        }
        let nsRange = NSRange(source.startIndex..., in: source)
        let matches = regex.matches(in: source, options: [], range: nsRange)

        for match in matches {
            guard let declRange = Range(match.range, in: source) else { continue }
            guard let openBrace = source.range(
                of: "{",
                range: declRange.upperBound..<source.endIndex
            ) else { continue }
            guard let closeBrace = matchingCloseBrace(in: source, openAt: openBrace.lowerBound) else {
                continue
            }
            let body = source[openBrace.upperBound..<closeBrace]
            if body.contains(sentinel) { continue }

            var result = source
            result.insert(contentsOf: "\\n" + stub, at: openBrace.upperBound)
            return result
        }
        return nil
    }

    private static func matchingCloseBrace(in source: String, openAt: String.Index) -> String.Index? {
        var depth = 0
        var idx = openAt
        while idx < source.endIndex {
            let c = source[idx]
            if c == "{" { depth += 1 }
            if c == "}" {
                depth -= 1
                if depth == 0 { return idx }
            }
            idx = source.index(after: idx)
        }
        return nil
    }
}
`;

  return header + regexFn + injectFn;
}

function regexCall(r: RegexFix): string {
  const pattern = swiftStringLiteral(r.pattern);
  const template = swiftStringLiteral(r.replacement);
  return `        // ${r.code} — ${r.description}
        do {
            let (next, count) = applyRegex(current, pattern: ${pattern}, template: ${template})
            if count > 0 {
                current = next
                applied.append(.init(code: "${r.code}", message: "${escapeMessage(r.description)}"))
            }
        }`;
}

function structCall(r: StructInjectFix): string {
  const stub = swiftMultilineLiteral(r.stub);
  return `        // ${r.code} — ${r.description}
        if let next = injectIntoStruct(
            current,
            conformance: "${r.conformance}",
            sentinel: "${escapeMessage(r.sentinel)}",
            stub: ${stub}
        ) {
            current = next
            applied.append(.init(code: "${r.code}", message: "${escapeMessage(r.description)}"))
        }`;
}

function swiftStringLiteral(s: string): string {
  // Produce a normal Swift "..." literal, escaping backslash and quotes.
  const escaped = s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
  return `"${escaped}"`;
}

function swiftMultilineLiteral(s: string): string {
  // Use a raw multiline literal with #"""..."""# so nothing needs escaping.
  return `#"""\n${s}\n"""#`;
}

function escapeMessage(s: string): string {
  return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}

function main() {
  const rendered = render();
  const check = process.argv.includes("--check");

  if (check) {
    const existing = readFileSync(TARGET, "utf8");
    if (existing !== rendered) {
      console.error(
        "AxintFixer.swift is stale. Run 'npm run gen:swift-fixer' and commit the result."
      );
      process.exit(1);
    }
    console.log("AxintFixer.swift is up to date.");
    return;
  }

  writeFileSync(TARGET, rendered);
  console.log(`wrote ${TARGET}`);
}

main();
