/**
 * migration_lint — PostgreSQL migration anti-pattern detection.
 *
 * Thin wrapper around the squawk CLI (https://github.com/sbdchd/squawk).
 * Finds migration SQL files in the index, runs squawk --reporter Json on them,
 * and aggregates findings by severity/rule.
 *
 * squawk is an optional external dependency; if it is not on PATH this tool
 * returns a friendly install hint rather than throwing.
 */
import { execFile } from "node:child_process";
import { join } from "node:path";
import { getCodeIndex } from "./index-tools.js";
import { matchFilePattern } from "../utils/glob.js";
// Default glob patterns covering common migration layouts.
const DEFAULT_MIGRATION_GLOBS = [
    "prisma/migrations/**/*.sql",
    "migrations/**/*.sql",
    "db/migrate/**/*.sql",
    "drizzle/**/*.sql",
];
const INSTALL_HINT = "squawk not found. Install: brew install squawk OR cargo install squawk-cli OR https://github.com/sbdchd/squawk#installation";
function runExecFile(cmd, args, opts) {
    return new Promise((resolve, reject) => {
        execFile(cmd, args, opts, (err, stdout, stderr) => {
            // Default encoding is utf-8 so stdout/stderr are strings here.
            const out = (stdout ?? "");
            const errOut = (stderr ?? "");
            if (err) {
                // Attach stdout/stderr so the caller can still parse JSON output even
                // when squawk exits non-zero (it exits non-zero when findings exist).
                const withIO = err;
                withIO.stdout = out;
                withIO.stderr = errOut;
                reject(err);
                return;
            }
            resolve({ stdout: out, stderr: errOut });
        });
    });
}
// ---------------------------------------------------------------------------
// Main entry point
// ---------------------------------------------------------------------------
export async function migrationLint(repo, options) {
    const index = await getCodeIndex(repo);
    if (!index)
        throw new Error(`Repository "${repo}" not found.`);
    // ---- 1. Find migration files in the index -------------------------------
    const globs = options?.migration_glob
        ? [options.migration_glob]
        : DEFAULT_MIGRATION_GLOBS;
    const migrationFiles = index.files
        .filter((f) => globs.some((g) => matchFilePattern(f.path, g)))
        .map((f) => f.path);
    // No files — return an empty, non-error result.
    if (migrationFiles.length === 0) {
        return {
            squawk_installed: true,
            files_checked: 0,
            findings: [],
            by_severity: { error: 0, warning: 0 },
            by_rule: {},
        };
    }
    // ---- 2. Probe squawk --version to detect installation -------------------
    let squawkVersion;
    try {
        const { stdout } = await runExecFile("squawk", ["--version"], {
            maxBuffer: 1 << 20, // 1 MB
            timeout: 5_000,
        });
        squawkVersion = stdout.trim() || undefined;
    }
    catch (err) {
        if (isEnoent(err)) {
            return {
                squawk_installed: false,
                files_checked: migrationFiles.length,
                findings: [],
                by_severity: { error: 0, warning: 0 },
                by_rule: {},
                install_hint: INSTALL_HINT,
            };
        }
        // Some other problem (e.g. squawk crashed on --version). Treat as not
        // usable but surface the hint so the agent can guide the user.
        return {
            squawk_installed: false,
            files_checked: migrationFiles.length,
            findings: [],
            by_severity: { error: 0, warning: 0 },
            by_rule: {},
            install_hint: INSTALL_HINT,
        };
    }
    // ---- 3. Build squawk CLI args ------------------------------------------
    const cliArgs = ["--reporter", "Json"];
    if (options?.pg_version) {
        cliArgs.push("--pg-version", options.pg_version);
    }
    if (options?.excluded_rules && options.excluded_rules.length > 0) {
        cliArgs.push("--exclude", options.excluded_rules.join(","));
    }
    // Pass absolute paths so squawk does not depend on CWD.
    for (const relPath of migrationFiles) {
        cliArgs.push(join(index.root, relPath));
    }
    // ---- 4. Run squawk ------------------------------------------------------
    let stdout = "";
    try {
        const res = await runExecFile("squawk", cliArgs, {
            maxBuffer: 32 * 1024 * 1024, // 32 MB
            timeout: 60_000,
        });
        stdout = res.stdout;
    }
    catch (err) {
        // squawk exits non-zero when there are findings — that's expected.
        // The stdout is attached to the error by runExecFile.
        const attached = err.stdout;
        if (attached) {
            stdout = attached;
        }
        else if (isEnoent(err)) {
            return {
                squawk_installed: false,
                files_checked: migrationFiles.length,
                findings: [],
                by_severity: { error: 0, warning: 0 },
                by_rule: {},
                install_hint: INSTALL_HINT,
            };
        }
        else {
            // Surface a structured error result so agents can still reason about it.
            const result = {
                squawk_installed: true,
                files_checked: migrationFiles.length,
                findings: [],
                by_severity: { error: 0, warning: 0 },
                by_rule: {},
            };
            if (squawkVersion)
                result.squawk_version = squawkVersion;
            return result;
        }
    }
    // ---- 5. Parse squawk JSON ----------------------------------------------
    const raw = parseSquawkOutput(stdout);
    const rootPrefix = index.root.endsWith("/") ? index.root : index.root + "/";
    const findings = raw.map((r) => {
        // Normalise file paths back to repo-relative when possible.
        let filePath = r.file;
        if (filePath.startsWith(rootPrefix)) {
            filePath = filePath.slice(rootPrefix.length);
        }
        const level = String(r.level).toLowerCase() === "error" ? "error" : "warning";
        const finding = {
            file: filePath,
            line: typeof r.line === "number" ? r.line : Number(r.line ?? 0) || 0,
            rule: String(r.rule_name ?? r.rule ?? "unknown"),
            level,
            message: String(r.message ?? ""),
        };
        if (r.url)
            finding.url = String(r.url);
        return finding;
    });
    // ---- 6. Aggregate -------------------------------------------------------
    const by_severity = { error: 0, warning: 0 };
    const by_rule = {};
    for (const f of findings) {
        by_severity[f.level] += 1;
        by_rule[f.rule] = (by_rule[f.rule] ?? 0) + 1;
    }
    const result = {
        squawk_installed: true,
        files_checked: migrationFiles.length,
        findings,
        by_severity,
        by_rule,
    };
    if (squawkVersion)
        result.squawk_version = squawkVersion;
    return result;
}
/**
 * squawk --reporter Json can emit either a single JSON array or one JSON
 * object per line (NDJSON). We handle both.
 */
function parseSquawkOutput(stdout) {
    const trimmed = stdout.trim();
    if (!trimmed)
        return [];
    // Try array first
    if (trimmed.startsWith("[")) {
        try {
            const parsed = JSON.parse(trimmed);
            if (Array.isArray(parsed))
                return parsed;
        }
        catch {
            // fall through to NDJSON
        }
    }
    // Try single object
    if (trimmed.startsWith("{")) {
        try {
            const parsed = JSON.parse(trimmed);
            if (Array.isArray(parsed))
                return parsed;
            if (parsed && typeof parsed === "object")
                return [parsed];
        }
        catch {
            // fall through to NDJSON
        }
    }
    // NDJSON — one object per line
    const results = [];
    for (const line of trimmed.split(/\r?\n/)) {
        const l = line.trim();
        if (!l)
            continue;
        try {
            const parsed = JSON.parse(l);
            if (Array.isArray(parsed)) {
                results.push(...parsed);
            }
            else if (parsed && typeof parsed === "object") {
                results.push(parsed);
            }
        }
        catch {
            // Skip non-JSON lines (squawk sometimes prints a header/footer).
        }
    }
    return results;
}
function isEnoent(err) {
    return (!!err
        && typeof err === "object"
        && "code" in err
        && err.code === "ENOENT");
}
//# sourceMappingURL=migration-lint-tools.js.map