import { spawnSync } from "child_process";
import { chmodSync, createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "fs";
import { arch, platform } from "os";
import { join } from "path";
import { Readable } from "stream";
import { pipeline } from "stream/promises";
import { APP_NAME, getBinDir } from "../config.js";
import { fetchWithRetry } from "./management-http.js";
const TOOLS_DIR = getBinDir();
const NETWORK_TIMEOUT_MS = 10_000;
const DOWNLOAD_TIMEOUT_MS = 120_000;
function isOfflineModeEnabled() {
    const value = process.env.PI_OFFLINE;
    if (!value)
        return false;
    return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes";
}
const TOOLS = {
    fd: {
        name: "fd",
        repo: "sharkdp/fd",
        binaryName: "fd",
        systemBinaryNames: ["fd", "fdfind"],
        tagPrefix: "v",
        getAssetName: (version, plat, architecture) => {
            if (plat === "darwin") {
                const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
                return `fd-v${version}-${archStr}-apple-darwin.tar.gz`;
            }
            else if (plat === "linux") {
                const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
                return `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`;
            }
            else if (plat === "win32") {
                const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
                return `fd-v${version}-${archStr}-pc-windows-msvc.zip`;
            }
            return null;
        },
    },
    rg: {
        name: "ripgrep",
        repo: "BurntSushi/ripgrep",
        binaryName: "rg",
        tagPrefix: "",
        getAssetName: (version, plat, architecture) => {
            if (plat === "darwin") {
                const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
                return `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`;
            }
            else if (plat === "linux") {
                if (architecture === "arm64") {
                    return `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`;
                }
                return `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;
            }
            else if (plat === "win32") {
                const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
                return `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`;
            }
            return null;
        },
    },
};
// Check if a command exists in PATH by trying to run it
function commandExists(cmd) {
    try {
        const result = spawnSync(cmd, ["--version"], { stdio: "pipe" });
        // Check for ENOENT error (command not found)
        return result.error === undefined || result.error === null;
    }
    catch {
        return false;
    }
}
// Get the path to a tool (system-wide or in our tools dir)
export function getToolPath(tool) {
    const config = TOOLS[tool];
    if (!config)
        return null;
    // Check our tools directory first
    const localPath = join(TOOLS_DIR, config.binaryName + (platform() === "win32" ? ".exe" : ""));
    if (existsSync(localPath)) {
        return localPath;
    }
    // Check system PATH - if found, just return the command name (it's in PATH)
    const systemBinaryNames = config.systemBinaryNames ?? [config.binaryName];
    for (const systemBinaryName of systemBinaryNames) {
        if (commandExists(systemBinaryName)) {
            return systemBinaryName;
        }
    }
    return null;
}
// Fetch latest release version from GitHub
async function getLatestVersion(repo) {
    const response = await fetchWithRetry(`https://api.github.com/repos/${repo}/releases/latest`, {
        headers: { "User-Agent": `${APP_NAME}-coding-agent` },
    }, { timeoutMs: NETWORK_TIMEOUT_MS });
    if (!response.ok) {
        throw new Error(`GitHub API error: ${response.status}`);
    }
    const data = (await response.json());
    return data.tag_name.replace(/^v/, "");
}
// Download a file from URL
async function downloadFile(url, dest) {
    const response = await fetchWithRetry(url, undefined, { timeoutMs: DOWNLOAD_TIMEOUT_MS });
    if (!response.ok) {
        throw new Error(`Failed to download: ${response.status}`);
    }
    if (!response.body) {
        throw new Error("No response body");
    }
    const fileStream = createWriteStream(dest);
    await pipeline(Readable.fromWeb(response.body), fileStream);
}
function findBinaryRecursively(rootDir, binaryFileName) {
    const stack = [rootDir];
    while (stack.length > 0) {
        const currentDir = stack.pop();
        if (!currentDir)
            continue;
        const entries = readdirSync(currentDir, { withFileTypes: true });
        for (const entry of entries) {
            const fullPath = join(currentDir, entry.name);
            if (entry.isFile() && entry.name === binaryFileName) {
                return fullPath;
            }
            if (entry.isDirectory()) {
                stack.push(fullPath);
            }
        }
    }
    return null;
}
function formatSpawnFailure(result) {
    if (result.error?.message) {
        return result.error.message;
    }
    const stderr = result.stderr?.toString().trim();
    if (stderr) {
        return stderr;
    }
    const stdout = result.stdout?.toString().trim();
    if (stdout) {
        return stdout;
    }
    return `exit status ${result.status ?? "unknown"}`;
}
function runExtractionCommand(command, args) {
    const result = spawnSync(command, args, { stdio: "pipe" });
    if (!result.error && result.status === 0) {
        return null;
    }
    return `${command}: ${formatSpawnFailure(result)}`;
}
function extractTarGzArchive(archivePath, extractDir, assetName) {
    const failure = runExtractionCommand("tar", ["xzf", archivePath, "-C", extractDir]);
    if (failure) {
        throw new Error(`Failed to extract ${assetName}: ${failure}`);
    }
}
function getWindowsTarCommand() {
    const systemRoot = process.env.SystemRoot ?? process.env.WINDIR;
    if (systemRoot) {
        const systemTar = join(systemRoot, "System32", "tar.exe");
        if (existsSync(systemTar)) {
            return systemTar;
        }
    }
    return "tar.exe";
}
function extractZipArchive(archivePath, extractDir, assetName) {
    const failures = [];
    if (platform() === "win32") {
        // Windows ships bsdtar as tar.exe, which supports zip files. Prefer the
        // System32 binary over Git Bash's GNU tar, which does not handle zip archives.
        const tarFailure = runExtractionCommand(getWindowsTarCommand(), ["xf", archivePath, "-C", extractDir]);
        if (!tarFailure)
            return;
        failures.push(tarFailure);
        const script = "& { param($archive, $destination) $ErrorActionPreference = 'Stop'; Expand-Archive -LiteralPath $archive -DestinationPath $destination -Force }";
        const powershellFailure = runExtractionCommand("powershell.exe", [
            "-NoLogo",
            "-NoProfile",
            "-NonInteractive",
            "-ExecutionPolicy",
            "Bypass",
            "-Command",
            script,
            archivePath,
            extractDir,
        ]);
        if (!powershellFailure)
            return;
        failures.push(powershellFailure);
    }
    else {
        const unzipFailure = runExtractionCommand("unzip", ["-q", archivePath, "-d", extractDir]);
        if (!unzipFailure)
            return;
        failures.push(unzipFailure);
        const tarFailure = runExtractionCommand("tar", ["xf", archivePath, "-C", extractDir]);
        if (!tarFailure)
            return;
        failures.push(tarFailure);
    }
    throw new Error(`Failed to extract ${assetName}: ${failures.join("; ")}`);
}
// Download and install a tool
async function downloadTool(tool) {
    const config = TOOLS[tool];
    if (!config)
        throw new Error(`Unknown tool: ${tool}`);
    const plat = platform();
    const architecture = arch();
    // Get latest version
    let version = await getLatestVersion(config.repo);
    if (tool === "fd" && plat === "darwin" && architecture === "x64") {
        version = "10.3.0";
    }
    // Get asset name for this platform
    const assetName = config.getAssetName(version, plat, architecture);
    if (!assetName) {
        throw new Error(`Unsupported platform: ${plat}/${architecture}`);
    }
    // Create tools directory
    mkdirSync(TOOLS_DIR, { recursive: true });
    const downloadUrl = `https://github.com/${config.repo}/releases/download/${config.tagPrefix}${version}/${assetName}`;
    const archivePath = join(TOOLS_DIR, assetName);
    const binaryExt = plat === "win32" ? ".exe" : "";
    const binaryPath = join(TOOLS_DIR, config.binaryName + binaryExt);
    // Download
    await downloadFile(downloadUrl, archivePath);
    // Extract into a unique temp directory. fd and rg downloads can run concurrently
    // during startup, so sharing a fixed directory causes races.
    const extractDir = join(TOOLS_DIR, `extract_tmp_${config.binaryName}_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`);
    mkdirSync(extractDir, { recursive: true });
    try {
        if (assetName.endsWith(".tar.gz")) {
            extractTarGzArchive(archivePath, extractDir, assetName);
        }
        else if (assetName.endsWith(".zip")) {
            extractZipArchive(archivePath, extractDir, assetName);
        }
        else {
            throw new Error(`Unsupported archive format: ${assetName}`);
        }
        // Find the binary in extracted files. Some archives contain files directly
        // at root, others nest under a versioned subdirectory.
        const binaryFileName = config.binaryName + binaryExt;
        const extractedDir = join(extractDir, assetName.replace(/\.(tar\.gz|zip)$/, ""));
        const extractedBinaryCandidates = [join(extractedDir, binaryFileName), join(extractDir, binaryFileName)];
        let extractedBinary = extractedBinaryCandidates.find((candidate) => existsSync(candidate));
        if (!extractedBinary) {
            extractedBinary = findBinaryRecursively(extractDir, binaryFileName) ?? undefined;
        }
        if (extractedBinary) {
            renameSync(extractedBinary, binaryPath);
        }
        else {
            throw new Error(`Binary not found in archive: expected ${binaryFileName} under ${extractDir}`);
        }
        // Make executable (Unix only)
        if (plat !== "win32") {
            chmodSync(binaryPath, 0o755);
        }
    }
    finally {
        // Cleanup
        rmSync(archivePath, { force: true });
        rmSync(extractDir, { recursive: true, force: true });
    }
    return binaryPath;
}
// Termux package names for tools
const TERMUX_PACKAGES = {
    fd: "fd",
    rg: "ripgrep",
};
/**
 * Ensure a tool is available, downloading if necessary.
 * Reports progress through `onStatus`; status messages are otherwise silent.
 * Returns the tool path, or undefined if unavailable.
 */
export async function ensureTool(tool, onStatus) {
    const existingPath = getToolPath(tool);
    if (existingPath) {
        return existingPath;
    }
    const config = TOOLS[tool];
    if (!config)
        return undefined;
    if (isOfflineModeEnabled()) {
        onStatus?.({ type: "warning", message: `${config.name} not found. Offline mode enabled, skipping download.` });
        return undefined;
    }
    // On Android/Termux, Linux binaries don't work due to Bionic libc incompatibility.
    // Users must install via pkg.
    if (platform() === "android") {
        const pkgName = TERMUX_PACKAGES[tool] ?? tool;
        onStatus?.({ type: "warning", message: `${config.name} not found. Install with: pkg install ${pkgName}` });
        return undefined;
    }
    // Tool not found - download it
    onStatus?.({ type: "info", message: `${config.name} not found. Downloading...` });
    try {
        const path = await downloadTool(tool);
        onStatus?.({ type: "info", message: `${config.name} installed to ${path}` });
        return path;
    }
    catch (e) {
        onStatus?.({
            type: "warning",
            message: `Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`,
        });
        return undefined;
    }
}
//# sourceMappingURL=tools-manager.js.map