import { createHash, randomUUID } from "node:crypto";
import { spawnSync } from "node:child_process";
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, chmodSync, readdirSync } from "node:fs";
import { createWriteStream } from "node:fs";
import { arch as osArch, homedir as osHomedir } from "node:os";
import { delimiter, join } from "node:path";
import { Readable } from "node:stream";
import { finished } from "node:stream/promises";
import extractZip from "extract-zip";
export const RTK_VERSION = "0.33.1";
export const NERF_RTK_DISABLED_ENV = "NERF_RTK_DISABLED";
export const NERF_SKIP_RTK_INSTALL_ENV = "NERF_SKIP_RTK_INSTALL";
export const NERF_RTK_PATH_ENV = "NERF_RTK_PATH";
export const RTK_TELEMETRY_DISABLED_ENV = "RTK_TELEMETRY_DISABLED";
const RTK_REPO = "rtk-ai/rtk";
const RTK_REWRITE_TIMEOUT_MS = 5_000;
function isTruthy(value) {
    if (!value)
        return false;
    const normalized = value.trim().toLowerCase();
    return normalized === "1" || normalized === "true" || normalized === "yes";
}
export function isRtkEnabled(env = process.env) {
    return !isTruthy(env[NERF_RTK_DISABLED_ENV]);
}
function resolveAppRoot(env = process.env) {
    return env.NERF_HOME || env.NERF_HOME || join(osHomedir(), ".nerf");
}
export function getManagedRtkDir(env = process.env) {
    return join(resolveAppRoot(env), "agent", "bin");
}
export function getRtkBinaryName(platform = process.platform) {
    return platform === "win32" ? "rtk.exe" : "rtk";
}
export function getManagedRtkPath(platform = process.platform, targetDir = getManagedRtkDir()) {
    return join(targetDir, getRtkBinaryName(platform));
}
export function prependPathEntry(env, entry) {
    const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path") ?? (process.platform === "win32" ? "Path" : "PATH");
    const currentPath = env[pathKey] ?? "";
    const parts = currentPath.split(delimiter).filter(Boolean);
    if (!parts.includes(entry)) {
        env[pathKey] = [entry, currentPath].filter(Boolean).join(delimiter);
    }
    return env;
}
export function applyRtkProcessEnv(env = process.env) {
    prependPathEntry(env, getManagedRtkDir(env));
    env[RTK_TELEMETRY_DISABLED_ENV] = "1";
    return env;
}
function getPathValue(env) {
    const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path");
    return pathKey ? env[pathKey] : env.PATH;
}
export function buildRtkEnv(env = process.env) {
    return applyRtkProcessEnv({ ...env });
}
export function resolveRtkAssetName(platform, arch, version = RTK_VERSION) {
    void version;
    if (platform === "darwin" && arch === "arm64")
        return "rtk-aarch64-apple-darwin.tar.gz";
    if (platform === "darwin" && arch === "x64")
        return "rtk-x86_64-apple-darwin.tar.gz";
    if (platform === "linux" && arch === "arm64")
        return "rtk-aarch64-unknown-linux-gnu.tar.gz";
    if (platform === "linux" && arch === "x64")
        return "rtk-x86_64-unknown-linux-musl.tar.gz";
    if (platform === "win32" && arch === "x64")
        return "rtk-x86_64-pc-windows-msvc.zip";
    return null;
}
function getReleaseBaseUrl(version) {
    return `https://github.com/${RTK_REPO}/releases/download/v${version}`;
}
function getChecksumsUrl(version) {
    return `${getReleaseBaseUrl(version)}/checksums.txt`;
}
function buildAssetUrl(version, assetName) {
    return `${getReleaseBaseUrl(version)}/${assetName}`;
}
function parseChecksums(content) {
    const checksums = new Map();
    for (const rawLine of content.split(/\r?\n/)) {
        const line = rawLine.trim();
        if (!line)
            continue;
        const match = line.match(/^([a-f0-9]{64})\s+(.+)$/i);
        if (!match)
            continue;
        checksums.set(match[2], match[1].toLowerCase());
    }
    return checksums;
}
function sha256File(path) {
    const hash = createHash("sha256");
    hash.update(readFileSync(path));
    return hash.digest("hex");
}
async function downloadToFile(url, destination) {
    const response = await fetch(url, {
        headers: { "User-Agent": "nerf-rtk" },
        signal: AbortSignal.timeout(120_000),
    }).catch((err) => {
        const name = err?.name;
        if (name === "TimeoutError" || name === "AbortError") {
            throw new Error(`RTK binary download timed out after 120s — check network connection: ${url}`);
        }
        throw err;
    });
    if (!response.ok) {
        throw new Error(`download failed (${response.status}) for ${url}`);
    }
    if (!response.body) {
        throw new Error(`download returned no body for ${url}`);
    }
    const output = createWriteStream(destination);
    await finished(Readable.fromWeb(response.body).pipe(output));
}
function findBinaryRecursively(rootDir, binaryName) {
    const stack = [rootDir];
    while (stack.length > 0) {
        const current = stack.pop();
        if (!current)
            continue;
        const entries = readdirSync(current, { withFileTypes: true });
        for (const entry of entries) {
            const fullPath = join(current, entry.name);
            if (entry.isFile() && entry.name === binaryName) {
                return fullPath;
            }
            if (entry.isDirectory()) {
                stack.push(fullPath);
            }
        }
    }
    return null;
}
function extractArchive(assetName, archivePath, extractDir) {
    if (!assetName.endsWith(".tar.gz")) {
        throw new Error(`unsupported RTK archive format: ${assetName}`);
    }
    mkdirSync(extractDir, { recursive: true });
    const result = spawnSync("tar", ["xzf", archivePath, "-C", extractDir], {
        encoding: "utf-8",
        timeout: 30_000,
    });
    if (result.error || result.status !== 0) {
        throw new Error(result.error?.message ?? result.stderr?.trim() ?? `tar extraction failed for ${assetName}`);
    }
}
async function extractArchiveAsync(assetName, archivePath, extractDir) {
    if (assetName.endsWith(".zip")) {
        mkdirSync(extractDir, { recursive: true });
        await extractZip(archivePath, { dir: extractDir });
        return;
    }
    extractArchive(assetName, archivePath, extractDir);
}
function resolvePathCandidates(pathValue) {
    if (!pathValue)
        return [];
    return pathValue
        .split(delimiter)
        .map((part) => part.trim())
        .filter(Boolean);
}
function resolveSystemRtkPath(pathValue, platform = process.platform) {
    const candidates = platform === "win32"
        ? ["rtk.exe", "rtk.cmd", "rtk.bat", "rtk"]
        : ["rtk"];
    for (const dir of resolvePathCandidates(pathValue)) {
        for (const candidate of candidates) {
            const fullPath = join(dir, candidate);
            if (existsSync(fullPath)) {
                return fullPath;
            }
        }
    }
    return null;
}
export function resolveRtkBinaryPath(options = {}) {
    const env = options.env ?? process.env;
    const platform = options.platform ?? process.platform;
    if (options.binaryPath)
        return options.binaryPath;
    const explicitPath = env[NERF_RTK_PATH_ENV];
    if (explicitPath && existsSync(explicitPath)) {
        return explicitPath;
    }
    const managedPath = getManagedRtkPath(platform, options.targetDir ?? getManagedRtkDir(env));
    if (existsSync(managedPath)) {
        return managedPath;
    }
    // On Windows, also check for rtk.cmd in the managed dir (used by test fake RTK
    // and any wrapper-style installs where a .cmd launcher accompanies the binary).
    if (platform === "win32") {
        const managedDir = options.targetDir ?? getManagedRtkDir(env);
        const managedCmd = join(managedDir, "rtk.cmd");
        if (existsSync(managedCmd)) {
            return managedCmd;
        }
    }
    return resolveSystemRtkPath(options.pathValue ?? getPathValue(env), platform);
}
export function rewriteCommandWithRtk(command, options = {}) {
    if (!command.trim())
        return command;
    if (!isRtkEnabled(options.env ?? process.env))
        return command;
    const env = options.env ?? process.env;
    const binaryPath = resolveRtkBinaryPath({
        env,
        binaryPath: options.binaryPath,
    });
    if (!binaryPath)
        return command;
    const run = options.spawnSyncImpl ?? spawnSync;
    const result = run(binaryPath, ["rewrite", command], {
        encoding: "utf-8",
        env: buildRtkEnv(options.env ?? process.env),
        stdio: ["ignore", "pipe", "ignore"],
        timeout: options.timeoutMs ?? RTK_REWRITE_TIMEOUT_MS,
        // .cmd/.bat wrappers (used by fake-rtk in tests) require shell:true on Windows
        shell: /\.(cmd|bat)$/i.test(binaryPath),
    });
    if (result.error)
        return command;
    if (result.status !== 0 && result.status !== 3)
        return command;
    const rewritten = (result.stdout ?? "").trimEnd();
    return rewritten || command;
}
export function validateRtkBinary(binaryPath, options = {}) {
    const run = options.spawnSyncImpl ?? spawnSync;
    const result = run(binaryPath, ["rewrite", "git status"], {
        encoding: "utf-8",
        env: buildRtkEnv(options.env ?? process.env),
        stdio: ["ignore", "pipe", "ignore"],
        timeout: RTK_REWRITE_TIMEOUT_MS,
    });
    if (result.error)
        return false;
    if (result.status !== 0)
        return false;
    return (result.stdout ?? "").trim() === "rtk git status";
}
export async function ensureRtkAvailable(options = {}) {
    const env = options.env ?? process.env;
    if (!isRtkEnabled(env)) {
        return { enabled: false, supported: true, available: false, source: "disabled", reason: `${NERF_RTK_DISABLED_ENV} is set` };
    }
    if (isTruthy(env[NERF_SKIP_RTK_INSTALL_ENV])) {
        const configuredPath = env[NERF_RTK_PATH_ENV];
        if (configuredPath && existsSync(configuredPath)) {
            return { enabled: true, supported: true, available: true, source: "managed", binaryPath: configuredPath };
        }
        return { enabled: true, supported: true, available: false, source: "missing", reason: `${NERF_SKIP_RTK_INSTALL_ENV} is set` };
    }
    const targetDir = options.targetDir ?? getManagedRtkDir(env);
    const managedPath = getManagedRtkPath(process.platform, targetDir);
    if (existsSync(managedPath) && validateRtkBinary(managedPath, { env })) {
        return { enabled: true, supported: true, available: true, source: "managed", binaryPath: managedPath };
    }
    const systemPath = resolveSystemRtkPath(options.pathValue ?? getPathValue(env));
    if (systemPath && validateRtkBinary(systemPath, { env })) {
        return { enabled: true, supported: true, available: true, source: "system", binaryPath: systemPath };
    }
    const version = options.releaseVersion ?? RTK_VERSION;
    const assetName = resolveRtkAssetName(process.platform, osArch(), version);
    if (!assetName) {
        return {
            enabled: true,
            supported: false,
            available: false,
            source: "unsupported",
            reason: `RTK release asset unavailable for ${process.platform}/${osArch()}`,
        };
    }
    if (options.allowDownload === false) {
        return { enabled: true, supported: true, available: false, source: "missing", reason: "download disabled" };
    }
    mkdirSync(targetDir, { recursive: true });
    const tempRoot = join(targetDir, `.rtk-install-${randomUUID().slice(0, 8)}`);
    const archivePath = join(tempRoot, assetName);
    const extractDir = join(tempRoot, "extract");
    mkdirSync(tempRoot, { recursive: true });
    try {
        const checksumsUrl = getChecksumsUrl(version);
        const checksumsResponse = await fetch(checksumsUrl, { headers: { "User-Agent": "nerf-rtk" }, signal: AbortSignal.timeout(30_000) });
        if (!checksumsResponse.ok) {
            throw new Error(`failed to fetch RTK checksums (${checksumsResponse.status})`);
        }
        const checksums = parseChecksums(await checksumsResponse.text());
        const expectedSha = checksums.get(assetName);
        if (!expectedSha) {
            throw new Error(`missing checksum for ${assetName}`);
        }
        await downloadToFile(buildAssetUrl(version, assetName), archivePath);
        const actualSha = sha256File(archivePath);
        if (actualSha !== expectedSha) {
            throw new Error(`checksum mismatch for ${assetName}`);
        }
        await extractArchiveAsync(assetName, archivePath, extractDir);
        const extractedBinary = findBinaryRecursively(extractDir, getRtkBinaryName(process.platform));
        if (!extractedBinary) {
            throw new Error(`RTK binary not found in ${assetName}`);
        }
        copyFileSync(extractedBinary, managedPath);
        if (process.platform !== "win32") {
            chmodSync(managedPath, 0o755);
        }
        if (!validateRtkBinary(managedPath, { env })) {
            rmSync(managedPath, { force: true });
            throw new Error("downloaded RTK binary failed validation");
        }
        options.log?.(`installed RTK ${version} to ${managedPath}`);
        return { enabled: true, supported: true, available: true, source: "downloaded", binaryPath: managedPath };
    }
    catch (error) {
        options.log?.(`RTK install skipped: ${error instanceof Error ? error.message : String(error)}`);
        return {
            enabled: true,
            supported: true,
            available: false,
            source: "missing",
            reason: error instanceof Error ? error.message : String(error),
        };
    }
    finally {
        rmSync(tempRoot, { recursive: true, force: true });
    }
}
export async function bootstrapRtk(options = {}) {
    const result = await ensureRtkAvailable(options);
    applyRtkProcessEnv(process.env);
    if (result.binaryPath) {
        process.env[NERF_RTK_PATH_ENV] = result.binaryPath;
    }
    return result;
}
