/**
 * Deploy fork wrapper
 *
 * This module implements a fork-based deployment wrapper that:
 * 1. Spawns the deploy command as a child process using bunx
 * 2. Tees stdout/stderr to both the terminal and a Pulse stream
 * 3. On failure, sends diagnostics to the API
 *
 * This approach captures crashes, Bun runtime issues, and all output
 * for debugging failed deployments.
 */
import { spawn } from 'bun';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { appendFileSync, createWriteStream, existsSync, readFileSync, unlinkSync } from 'node:fs';
import { getUserAgent } from '../../api';
import { isUnicode } from '../../tui/symbols';
import { getAgentEnv } from '../../agent-detection';
import { projectDeploymentFail } from '@agentuity/server';
/**
 * Stream data to a Pulse stream URL.
 * Accepts a string, Blob/BunFile, or ReadableStream as the body to avoid
 * loading large outputs into memory.
 */
async function streamToPulse(streamURL, sdkKey, data, logger) {
    try {
        const response = await fetch(streamURL, {
            method: 'PUT',
            headers: {
                'Content-Type': 'text/plain',
                Authorization: `Bearer ${sdkKey}`,
                'User-Agent': getUserAgent(),
            },
            body: data,
        });
        if (!response.ok) {
            logger.error('Failed to stream to Pulse: %s', response.status);
        }
    }
    catch (err) {
        logger.error('Error streaming to Pulse: %s', err);
    }
}
/**
 * Run the deploy command as a forked child process
 */
export async function runForkedDeploy(options) {
    const { projectDir, apiClient, logger, sdkKey, deployment, args } = options;
    const deploymentId = deployment.id;
    const buildLogsStreamURL = deployment.buildLogsStreamURL;
    const reportFile = join(tmpdir(), `agentuity-deploy-${deploymentId}.json`);
    const cleanLogsFile = join(tmpdir(), `agentuity-deploy-${deploymentId}-logs.txt`);
    const rawLogsFile = join(tmpdir(), `agentuity-deploy-${deploymentId}-raw.txt`);
    const deployResultFile = join(tmpdir(), `agentuity-deploy-${deploymentId}-result.json`);
    const rawLogsWriter = createWriteStream(rawLogsFile);
    let proc = null;
    let cancelled = false;
    // Signal handler to forward signals to child process and report cancellation
    const handleSignal = async (signal) => {
        if (cancelled)
            return;
        cancelled = true;
        logger.debug('Received %s, forwarding to child process', signal);
        // Kill the child process if it's still running
        if (proc && proc.exitCode === null) {
            try {
                proc.kill(signal);
            }
            catch (err) {
                logger.debug('Failed to kill child process: %s', err);
            }
        }
        // Report deployment as cancelled (with timeout to ensure prompt exit)
        const cancelMessage = 'Deployment cancelled by user';
        const timeoutMs = 3000; // 3 second timeout
        const timeoutPromise = new Promise((resolve) => {
            setTimeout(() => {
                logger.debug('API call to report cancellation timed out after %dms', timeoutMs);
                resolve();
            }, timeoutMs);
        });
        const apiCallPromise = projectDeploymentFail(apiClient, deploymentId, {
            error: cancelMessage,
            diagnostics: {
                success: false,
                errors: [
                    {
                        type: 'general',
                        scope: 'deploy',
                        message: cancelMessage,
                        code: 'DEPLOY_CANCELLED',
                    },
                ],
                warnings: [],
                diagnostics: [],
                error: cancelMessage,
            },
        }).catch((err) => {
            logger.debug('Failed to report cancellation: %s', err);
        });
        // Race API call against timeout to ensure prompt exit
        await Promise.race([apiCallPromise, timeoutPromise]);
        // Exit with signal-specific exit code
        const signalExitCodes = {
            SIGINT: 130, // 128 + 2
            SIGTERM: 143, // 128 + 15
            SIGHUP: 129, // 128 + 1
            SIGQUIT: 131, // 128 + 3
        };
        const exitCode = signalExitCodes[signal] ?? 128;
        process.exit(exitCode);
    };
    // Install signal handlers
    const sigintHandler = () => {
        void handleSignal('SIGINT');
    };
    const sigtermHandler = () => {
        void handleSignal('SIGTERM');
    };
    process.on('SIGINT', sigintHandler);
    process.on('SIGTERM', sigtermHandler);
    try {
        const childArgs = ['deploy', '--child-mode', `--report-file=${reportFile}`, ...args];
        // Pass the deployment info via environment variable (same format as CI builds)
        const deploymentEnvValue = JSON.stringify({
            id: deployment.id,
            orgId: deployment.orgId,
            publicKey: deployment.publicKey,
        });
        // Re-exec the same entry point that the parent is running so that
        // local/dev builds test the current code instead of a stale global
        // install.  process.execPath is the bun binary; Bun.main is the
        // script entry (e.g. bin/cli.ts or the compiled binary).
        const cmd = [process.execPath, Bun.main, ...childArgs];
        logger.debug('Spawning child deploy process: %s', cmd.join(' '));
        // Get terminal dimensions to pass to child
        const columns = process.stdout.columns || 80;
        const rows = process.stdout.rows || 24;
        proc = spawn({
            cmd,
            cwd: projectDir,
            env: {
                ...process.env,
                ...getAgentEnv(),
                AGENTUITY_FORK_PARENT: '1',
                AGENTUITY_DEPLOYMENT: deploymentEnvValue,
                // Force color and unicode output since child stdout/stderr are piped (not TTY)
                FORCE_COLOR: '1',
                // Only force unicode if parent terminal supports it
                ...(isUnicode ? { FORCE_UNICODE: '1' } : {}),
                // Pass terminal dimensions
                COLUMNS: String(columns),
                LINES: String(rows),
                // Enable clean log collection for Pulse streaming
                AGENTUITY_CLEAN_LOGS_FILE: cleanLogsFile,
                // Pass result file path for child to write deploy URLs/logs back
                AGENTUITY_DEPLOY_RESULT_FILE: deployResultFile,
            },
            stdin: 'inherit',
            stdout: 'pipe',
            stderr: 'pipe',
        });
        const handleOutput = async (stream, isStderr) => {
            const reader = stream.getReader();
            const target = isStderr ? process.stderr : process.stdout;
            try {
                while (true) {
                    const { done, value } = await reader.read();
                    if (done)
                        break;
                    // Stream raw bytes to disk instead of accumulating in memory.
                    // This prevents OOM / ERR_STRING_TOO_LONG crashes on large builds.
                    rawLogsWriter.write(value);
                    target.write(value);
                }
            }
            catch (err) {
                logger.debug('Stream read error: %s', err);
            }
        };
        const stdoutPromise = proc.stdout && typeof proc.stdout !== 'number'
            ? handleOutput(proc.stdout, false)
            : Promise.resolve();
        const stderrPromise = proc.stderr && typeof proc.stderr !== 'number'
            ? handleOutput(proc.stderr, true)
            : Promise.resolve();
        await Promise.all([stdoutPromise, stderrPromise]);
        // Close the raw logs writer so the file is fully flushed before reading
        await new Promise((resolve) => {
            rawLogsWriter.end(resolve);
        });
        const exitCode = await proc.exited;
        logger.debug('Child process exited with code: %d', exitCode);
        let diagnostics;
        if (existsSync(reportFile)) {
            try {
                const reportContent = readFileSync(reportFile, 'utf-8');
                diagnostics = JSON.parse(reportContent);
                unlinkSync(reportFile);
            }
            catch (err) {
                logger.debug('Failed to read report file: %s', err);
            }
        }
        // Read deploy result (URLs, logs) from child process
        let deployResult;
        if (existsSync(deployResultFile)) {
            try {
                const resultContent = readFileSync(deployResultFile, 'utf-8');
                deployResult = JSON.parse(resultContent);
                unlinkSync(deployResultFile);
            }
            catch (err) {
                logger.debug('Failed to read deploy result file: %s', err);
            }
        }
        // Stream clean logs to Pulse (prefer clean logs over raw output)
        if (buildLogsStreamURL) {
            let streamedCleanLogs = false;
            if (existsSync(cleanLogsFile)) {
                try {
                    const cleanLogs = Bun.file(cleanLogsFile);
                    if (cleanLogs.size > 0) {
                        await streamToPulse(buildLogsStreamURL, sdkKey, cleanLogs, logger);
                        streamedCleanLogs = true;
                    }
                    unlinkSync(cleanLogsFile);
                }
                catch (err) {
                    logger.debug('Failed to stream clean logs file: %s', err);
                }
            }
            if (!streamedCleanLogs && existsSync(rawLogsFile)) {
                // Stream raw logs file directly to Pulse without loading into memory
                await streamToPulse(buildLogsStreamURL, sdkKey, Bun.file(rawLogsFile), logger);
            }
        }
        if (exitCode !== 0) {
            const errorMessage = `Deploy process exited with code ${exitCode}`;
            if (!diagnostics) {
                diagnostics = {
                    success: false,
                    errors: [
                        {
                            type: 'general',
                            scope: 'deploy',
                            message: errorMessage,
                            code: 'DEPLOY_CRASH',
                        },
                    ],
                    warnings: [],
                    diagnostics: [],
                    error: errorMessage,
                };
            }
            else if (!diagnostics.error) {
                diagnostics.error = errorMessage;
            }
            try {
                await projectDeploymentFail(apiClient, deploymentId, {
                    error: errorMessage,
                    diagnostics,
                });
            }
            catch (err) {
                logger.error('Failed to report deployment failure: %s', err);
            }
            return { success: false, exitCode, diagnostics };
        }
        return { success: true, exitCode, diagnostics, deployResult };
    }
    catch (err) {
        const errorMessage = err instanceof Error ? err.message : String(err);
        logger.error('Fork deploy error: %s', errorMessage);
        if (buildLogsStreamURL) {
            let streamedCleanLogs = false;
            if (existsSync(cleanLogsFile)) {
                try {
                    const cleanLogs = Bun.file(cleanLogsFile);
                    if (cleanLogs.size > 0) {
                        await streamToPulse(buildLogsStreamURL, sdkKey, cleanLogs, logger);
                        streamedCleanLogs = true;
                    }
                    unlinkSync(cleanLogsFile);
                }
                catch {
                    // ignore
                }
            }
            if (streamedCleanLogs) {
                await streamToPulse(buildLogsStreamURL, sdkKey, `\n\n--- FORK ERROR ---\n${errorMessage}\n`, logger);
            }
            else {
                // Append error to raw logs file and stream it without loading into memory
                try {
                    appendFileSync(rawLogsFile, `\n\n--- FORK ERROR ---\n${errorMessage}\n`);
                }
                catch {
                    // ignore — file may not exist if child never produced output
                }
                if (existsSync(rawLogsFile)) {
                    await streamToPulse(buildLogsStreamURL, sdkKey, Bun.file(rawLogsFile), logger);
                }
                else {
                    await streamToPulse(buildLogsStreamURL, sdkKey, `--- FORK ERROR ---\n${errorMessage}\n`, logger);
                }
            }
        }
        try {
            await projectDeploymentFail(apiClient, deploymentId, {
                error: errorMessage,
                diagnostics: {
                    success: false,
                    errors: [
                        {
                            type: 'general',
                            scope: 'deploy',
                            message: errorMessage,
                            code: 'DEPLOY_FORK_ERROR',
                        },
                    ],
                    warnings: [],
                    diagnostics: [],
                    error: errorMessage,
                },
            });
        }
        catch (failErr) {
            logger.error('Failed to report deployment failure: %s', failErr);
        }
        return {
            success: false,
            exitCode: 1,
            diagnostics: {
                success: false,
                errors: [
                    {
                        type: 'general',
                        scope: 'deploy',
                        message: errorMessage,
                        code: 'DEPLOY_FORK_ERROR',
                    },
                ],
                warnings: [],
                diagnostics: [],
                error: errorMessage,
            },
        };
    }
    finally {
        // Clean up signal handlers
        process.off('SIGINT', sigintHandler);
        process.off('SIGTERM', sigtermHandler);
        // Clean up temp files
        for (const file of [reportFile, cleanLogsFile, rawLogsFile, deployResultFile]) {
            if (existsSync(file)) {
                try {
                    unlinkSync(file);
                }
                catch {
                    // ignore
                }
            }
        }
    }
}
//# sourceMappingURL=deploy-fork.js.map