import { spawn } from 'bun';
import { mkdir, mkdtemp, readdir, realpath, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { ErrorCode } from '../../errors';
import * as tui from '../../tui';
export async function hasProjectDependenciesInstalled(projectDir) {
    let dir = projectDir;
    for (;;) {
        const bunStore = join(dir, 'node_modules', '.bun');
        const lockfile = join(dir, 'bun.lock');
        if ((await stat(bunStore).catch(() => null))?.isDirectory() &&
            (await Bun.file(lockfile).exists())) {
            return true;
        }
        const parent = dirname(dir);
        if (parent === dir) {
            return false;
        }
        dir = parent;
    }
}
async function runCommand(cmd, cwd) {
    const proc = spawn({
        cmd,
        cwd,
        env: { ...process.env, CI: 'true' },
        stdin: 'ignore',
        stdout: 'inherit',
        stderr: 'inherit',
    });
    await proc.exited;
    return proc.exitCode ?? 1;
}
const githubArchiveTokenEnv = 'GITHUB_ARCHIVE_TOKEN';
const githubArchiveHosts = new Set(['api.github.com', 'codeload.github.com']);
export function sourceDownloadHeaders(url) {
    const token = process.env[githubArchiveTokenEnv]?.trim();
    if (!token) {
        return undefined;
    }
    let parsed;
    try {
        parsed = new URL(url);
    }
    catch {
        return undefined;
    }
    if (!githubArchiveHosts.has(parsed.hostname.toLowerCase())) {
        return undefined;
    }
    return {
        Authorization: `Bearer ${token}`,
        Accept: 'application/vnd.github+json',
        'X-GitHub-Api-Version': '2022-11-28',
    };
}
async function fetchSourceArchive(url) {
    let currentURL = url;
    for (let redirectCount = 0; redirectCount <= 5; redirectCount++) {
        const response = await fetch(currentURL, {
            headers: sourceDownloadHeaders(currentURL),
            redirect: 'manual',
            signal: AbortSignal.timeout(60_000),
        });
        if (response.status < 300 || response.status >= 400) {
            return response;
        }
        const location = response.headers.get('location');
        if (!location) {
            return response;
        }
        currentURL = new URL(location, currentURL).toString();
    }
    throw new Error('Download failed: too many redirects');
}
export async function downloadSource(url, targetPath) {
    let lastError;
    for (let attempt = 1; attempt <= 3; attempt++) {
        try {
            const response = await fetchSourceArchive(url);
            if (!response.ok) {
                throw new Error(`Download failed with status ${response.status}`);
            }
            await Bun.write(targetPath, await response.arrayBuffer());
            return;
        }
        catch (error) {
            lastError = error;
            if (attempt < 3) {
                tui.info(`Download failed (attempt ${attempt}/3), retrying...`);
            }
        }
    }
    throw lastError instanceof Error ? lastError : new Error('Download failed');
}
export function buildDeployArgs(opts) {
    const args = [];
    if (opts.trigger)
        args.push('--trigger', opts.trigger);
    if (opts.event)
        args.push('--event', opts.event);
    if (opts.message)
        args.push('--message', opts.message);
    if (opts.commit)
        args.push('--commit', opts.commit);
    if (opts.commitUrl)
        args.push('--commit-url', opts.commitUrl);
    if (opts.branch)
        args.push('--branch', opts.branch);
    if (opts.repo)
        args.push('--repo', opts.repo);
    if (opts.provider)
        args.push('--provider', opts.provider);
    if (opts.pullRequestNumber !== undefined) {
        args.push('--pull-request-number', String(opts.pullRequestNumber));
    }
    if (opts.pullRequestUrl)
        args.push('--pull-request-url', opts.pullRequestUrl);
    if (opts.logsUrl)
        args.push('--logs-url', opts.logsUrl);
    if (opts.skipDnsValidation)
        args.push('--skip-dns-validation');
    if (opts.skipTypeCheck)
        args.push('--skip-type-check');
    return args;
}
export async function runCIBuild(opts, _logger) {
    let tempDir = '';
    // Track subprocess exit code so we can clean up in finally before calling process.exit().
    // Bun does not reliably honor process.exitCode, so an explicit process.exit() is required.
    let pendingExitCode;
    try {
        let projectDir;
        if (opts.url) {
            // Download and extract source from URL
            tempDir = await mkdtemp(join(tmpdir(), 'agentuity-ci-build-'));
            const sourceZipPath = join(tempDir, 'source.zip');
            const extractPath = join(tempDir, 'build');
            tui.info('1️⃣ Downloading source code from GitHub...');
            await downloadSource(opts.url, sourceZipPath);
            tui.info('2️⃣ Unzipping source code from GitHub...');
            await mkdir(extractPath, { recursive: true });
            const unzipExit = await runCommand(['unzip', '-q', sourceZipPath, '-d', extractPath], tempDir);
            if (unzipExit !== 0 && unzipExit !== 1) {
                tui.error(`Failed to unzip source archive (exit ${unzipExit})`);
                pendingExitCode = unzipExit;
                return;
            }
            const extractedEntries = await readdir(extractPath, { withFileTypes: true });
            const extractedDirs = extractedEntries.filter((entry) => entry.isDirectory());
            if (extractedDirs.length !== 1) {
                tui.fatal(`Expected one root directory after unzip, found ${extractedDirs.length}`, ErrorCode.BUILD_FAILED);
            }
            const sourceRoot = extractedDirs.at(0);
            if (!sourceRoot) {
                tui.fatal('Could not determine extracted source directory', ErrorCode.BUILD_FAILED);
            }
            const sourceRootDir = join(extractPath, sourceRoot.name);
            projectDir = sourceRootDir;
            if (opts.directory) {
                projectDir = join(sourceRootDir, opts.directory);
            }
            const projectStats = await stat(projectDir).catch(() => null);
            if (!projectStats?.isDirectory()) {
                tui.fatal(`Build directory not found: ${projectDir}`, ErrorCode.CONFIG_INVALID);
            }
            // Resolve symlinks and verify the project dir is within the source root
            const realProjectDir = await realpath(projectDir).catch(() => null);
            const realSourceRoot = await realpath(sourceRootDir).catch(() => null);
            if (!realProjectDir || !realSourceRoot || !realProjectDir.startsWith(realSourceRoot)) {
                tui.fatal('Directory path escapes the source root (path traversal denied)', ErrorCode.CONFIG_INVALID);
            }
            projectDir = realProjectDir;
        }
        else {
            // No URL — use current working directory (source already present, e.g. snapshot-based deploy)
            tui.info('1️⃣ Using local source (no download URL provided)...');
            projectDir = process.cwd();
            if (opts.directory) {
                projectDir = join(projectDir, opts.directory);
            }
        }
        const sdkKey = process.env.AGENTUITY_SDK_KEY;
        if (sdkKey) {
            await Bun.write(join(projectDir, '.env'), `AGENTUITY_SDK_KEY=${sdkKey}\n`);
        }
        if (await hasProjectDependenciesInstalled(projectDir)) {
            tui.info('3️⃣ Using existing project dependencies (skipping install)...');
        }
        else {
            tui.info('3️⃣ Installing your project dependencies...');
            const installExit = await runCommand(['bun', 'install'], projectDir);
            if (installExit !== 0) {
                tui.error(`Dependency installation failed (exit ${installExit})`);
                pendingExitCode = installExit;
                return;
            }
        }
        const packageJsonPath = join(projectDir, 'package.json');
        const packageJsonFile = Bun.file(packageJsonPath);
        let scripts;
        if (await packageJsonFile.exists()) {
            const packageJson = (await packageJsonFile.json());
            scripts = packageJson.scripts;
        }
        if (scripts?.predeploy) {
            tui.info('🔧 Running predeploy script...');
            const predeployExit = await runCommand(['bun', 'run', '--bun', 'predeploy'], projectDir);
            if (predeployExit !== 0) {
                tui.error(`Predeploy failed (exit ${predeployExit})`);
                pendingExitCode = predeployExit;
                return;
            }
        }
        tui.info('4️⃣ Deploying your project...');
        // Use the locally installed CLI binary instead of bunx to avoid
        // bunx resolution crashes on certain Bun versions (e.g. arm64 segfaults)
        const localCliBin = join(projectDir, 'node_modules', '.bin', 'agentuity');
        const cliExists = await stat(localCliBin).catch(() => null);
        const deployCmd = cliExists
            ? ['bun', localCliBin, 'deploy', ...buildDeployArgs(opts)]
            : ['bunx', '--bun', 'agentuity', 'deploy', ...buildDeployArgs(opts)];
        tui.info(`Using CLI: ${cliExists ? localCliBin : 'bunx --bun agentuity'}`);
        const deployExit = await runCommand(deployCmd, projectDir);
        if (deployExit !== 0) {
            tui.error(`Deploy failed (exit ${deployExit})`);
            pendingExitCode = deployExit;
            return;
        }
        if (scripts?.postdeploy) {
            tui.info('🔧 Running postdeploy script...');
            const postdeployExit = await runCommand(['bun', 'run', '--bun', 'postdeploy'], projectDir);
            if (postdeployExit !== 0) {
                tui.error(`Postdeploy failed (exit ${postdeployExit})`);
                pendingExitCode = postdeployExit;
                return;
            }
        }
        tui.success('✅ Your project has been built...');
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        tui.fatal(`CI build failed: ${message}`, ErrorCode.BUILD_FAILED);
    }
    finally {
        if (tempDir) {
            await rm(tempDir, { recursive: true, force: true });
        }
        if (pendingExitCode !== undefined) {
            process.exit(pendingExitCode);
        }
    }
}
//# sourceMappingURL=ci.js.map