import { execFileSync } from "child_process";
import { existsSync } from "fs";
import { readFile, writeFile, mkdir } from "fs/promises";
import { join } from "path";
import { getOmkResourceSettings } from "../util/resource-profile.js";
import { redactSecrets as redactSecretText } from "./secret-scanner.js";
const QUALITY_KEYS = ["lint", "typecheck", "test", "build"];
const SCRIPT_NAME_PATTERN = /^[A-Za-z0-9:_-]+$/;
const PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn", "bun"]);
const AUTO_SCRIPT_CANDIDATES = {
    lint: ["lint"],
    typecheck: ["typecheck", "check"],
    test: ["test"],
    build: ["build"],
};
const DEBUG = !!process.env.OMK_DEBUG;
function debugLog(...args) {
    if (DEBUG) {
        console.error("[quality-gate]", ...args);
    }
}
function redactQualityText(text) {
    return redactSecretText(text).redacted;
}
export function detectPackageManager(projectRoot) {
    if (existsSync(join(projectRoot, "pnpm-lock.yaml")))
        return "pnpm";
    if (existsSync(join(projectRoot, "yarn.lock")))
        return "yarn";
    if (existsSync(join(projectRoot, "bun.lockb")) || existsSync(join(projectRoot, "bun.lock")))
        return "bun";
    return "npm";
}
export function resolvePackageManagerExecutable(pm) {
    if (process.platform === "win32") {
        return `${pm}.cmd`;
    }
    return pm;
}
export async function readPackageScripts(projectRoot) {
    try {
        const packageJson = JSON.parse(await readFile(join(projectRoot, "package.json"), "utf-8"));
        if (!packageJson.scripts || typeof packageJson.scripts !== "object")
            return new Set();
        return new Set(Object.entries(packageJson.scripts)
            .filter(([, value]) => typeof value === "string")
            .map(([key]) => key));
    }
    catch {
        return undefined;
    }
}
export function getQualitySetting(config, key) {
    const regex = new RegExp(`^${key}\\s*=\\s*"([^"]+)"`, "m");
    return config.match(regex)?.[1]?.trim() ?? "auto";
}
export function resolveQualityCommand(setting, defaultScript, detectedPackageManager, availableScripts) {
    const normalized = setting.trim();
    if (normalized === "off") {
        return { command: "", skipped: true };
    }
    const parsed = normalized === "auto"
        ? { scriptName: resolveAutoScriptName(defaultScript, availableScripts) }
        : parseQualitySetting(normalized);
    if (!parsed || !SCRIPT_NAME_PATTERN.test(parsed.scriptName)) {
        return { command: "[blocked unsafe quality command]", blocked: true };
    }
    const packageManager = parsed.packageManager ?? detectedPackageManager;
    if (availableScripts && !availableScripts.has(parsed.scriptName)) {
        return {
            command: formatPackageScriptCommand(packageManager, parsed.scriptName),
            missing: true,
        };
    }
    return {
        command: formatPackageScriptCommand(packageManager, parsed.scriptName),
        executable: packageManager,
        args: ["run", parsed.scriptName],
    };
}
function resolveAutoScriptName(defaultScript, availableScripts) {
    const candidates = AUTO_SCRIPT_CANDIDATES[defaultScript];
    if (!availableScripts)
        return candidates[0];
    return candidates.find((script) => availableScripts.has(script)) ?? candidates[0];
}
export async function runQualityGate(projectRoot, config) {
    const resources = await getOmkResourceSettings();
    const packageManager = detectPackageManager(projectRoot);
    const availableScripts = await readPackageScripts(projectRoot);
    // Run gates sequentially to avoid dist race conditions
    const results = [];
    for (const gate of QUALITY_KEYS) {
        const command = resolveQualityCommand(getQualitySetting(config, gate), gate, packageManager, availableScripts);
        results.push(await runResolvedQualityCommand(gate, command, projectRoot, resources.shellMaxBufferBytes));
    }
    const [lint, typecheck, test, build] = results;
    return { lint, typecheck, test, build };
}
function parseQualitySetting(setting) {
    if (SCRIPT_NAME_PATTERN.test(setting)) {
        return { scriptName: setting };
    }
    const parts = setting.split(/\s+/);
    if (parts.length === 3 && isPackageManager(parts[0]) && parts[1] === "run") {
        return { packageManager: parts[0], scriptName: parts[2] };
    }
    if (parts.length === 2 && parts[0] === "yarn") {
        return { packageManager: "yarn", scriptName: parts[1] };
    }
    return null;
}
function isPackageManager(value) {
    return PACKAGE_MANAGERS.has(value);
}
function formatPackageScriptCommand(packageManager, scriptName) {
    return `${packageManager} run ${scriptName}`;
}
async function runResolvedQualityCommand(gateName, command, projectRoot, maxBuffer) {
    if (command.skipped) {
        return {
            command: "",
            exitCode: 0,
            stdout: "",
            stderr: "skipped (off)",
            status: "skipped",
            failureType: null,
        };
    }
    if (command.blocked) {
        const result = {
            command: command.command,
            exitCode: 126,
            stdout: "",
            stderr: "blocked unsafe quality command; use auto, off, a package script name, or '<package-manager> run <script>'",
            status: "blocked",
            failureType: "mcp-error",
        };
        await saveQualityGateLog(projectRoot, gateName, result);
        return result;
    }
    if (command.missing || !command.executable || !command.args) {
        const result = {
            command: command.command,
            exitCode: 127,
            stdout: "",
            stderr: "package.json script not found",
            status: "missing",
            failureType: "mcp-error",
        };
        await saveQualityGateLog(projectRoot, gateName, result);
        return result;
    }
    try {
        debugLog(`Running ${gateName}: ${command.command}`);
        const stdout = execFileSync(resolvePackageManagerExecutable(command.executable), command.args, {
            cwd: projectRoot,
            encoding: "utf-8",
            stdio: ["ignore", "pipe", "pipe"],
            timeout: 120000,
            maxBuffer,
            shell: process.platform === "win32",
        });
        const stdoutStr = redactQualityText(stdout ?? "");
        const result = {
            command: command.command,
            exitCode: 0,
            stdout: stdoutStr,
            stderr: "",
            status: "passed",
            failureType: null,
        };
        await saveQualityGateLog(projectRoot, gateName, result);
        return result;
    }
    catch (err) {
        const e = err;
        const stdoutStr = redactQualityText(String(e.stdout ?? ""));
        const stderrStr = redactQualityText(String(e.stderr ?? ""));
        const signal = e.signal ? `\nterminated by signal: ${e.signal}` : "";
        let status = "failed";
        let failureType = "exit-code";
        if (e.code === "ETIMEDOUT" || signal.includes("terminated by signal")) {
            status = "timeout";
            failureType = "timeout";
        }
        else if (e.status !== 0 && e.status !== undefined) {
            failureType = "exit-code";
        }
        debugLog(`${gateName} ${status}: ${failureType}`);
        const result = {
            command: command.command,
            exitCode: e.status ?? 1,
            stdout: stdoutStr,
            stderr: redactQualityText(`${stderrStr}${signal}`),
            status,
            failureType,
        };
        await saveQualityGateLog(projectRoot, gateName, result);
        return result;
    }
}
async function saveQualityGateLog(projectRoot, gateName, result) {
    try {
        const logsDir = join(projectRoot, ".omk", "logs", "quality-gate");
        await mkdir(logsDir, { recursive: true });
        const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
        const fileName = `${gateName}-${timestamp}.log`;
        const logPath = join(logsDir, fileName);
        const logContent = [
            `gate: ${gateName}`,
            `command: ${result.command || "(none)"}`,
            `status: ${result.status}`,
            `failureType: ${result.failureType ?? "none"}`,
            `exitCode: ${result.exitCode}`,
            `--- stdout ---`,
            redactQualityText(result.stdout || "(empty)"),
            `--- stderr ---`,
            redactQualityText(result.stderr || "(empty)"),
        ].join("\n");
        await writeFile(logPath, logContent, "utf-8");
        result.logPath = logPath;
    }
    catch (err) {
        debugLog("Failed to save quality gate log:", err);
    }
}
export function printQualityGateResults(results, printMode = false) {
    const lines = [];
    if (printMode) {
        lines.push("{\"qualityGates\":");
    }
    const entries = QUALITY_KEYS.map((key) => {
        const r = results[key];
        const icon = r.status === "passed" ? "✓" : r.status === "skipped" ? "○" : "✗";
        const failureDetail = r.failureType ? ` (${r.failureType})` : "";
        return `  ${icon} ${key}: ${r.status}${failureDetail}${r.logPath ? ` [log: ${r.logPath}]` : ""}`;
    });
    lines.push(...entries);
    if (printMode) {
        lines.push("}");
    }
    return lines.join("\n");
}
export function getQualityKeys() {
    return [...QUALITY_KEYS];
}
