import { createPublicKey } from 'node:crypto';
import { createReadStream, createWriteStream, existsSync, mkdirSync, unlinkSync, writeFileSync, } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { pipeline } from 'node:stream/promises';
import { createGzip } from 'node:zlib';
import { StructuredError } from '@agentuity/core';
import { getAppBaseURL, projectDeploymentComplete, projectDeploymentCreate, projectDeploymentMalwareCheck, projectDeploymentStatus, projectDeploymentUpdate, projectEnvUpdate, projectGet, projectUpdateRegion, validateResources, } from '@agentuity/server';
import { z } from 'zod';
import { getUserAgent } from '../../api';
import { BuildReportCollector, clearGlobalCollector, setGlobalCollector } from '../../build-report';
import { getCachedProject, setCachedProject } from '../../cache';
import { getCommand } from '../../command-prefix';
import { getDefaultConfigDir, getGlobalCatalystAPIClient, getStreamURL, loadBuildMetadata, loadProjectSDKKey, saveProjectDir, updateProjectConfig, } from '../../config';
import { encryptFIPSKEMDEMStream } from '../../crypto/box';
import * as domain from '../../domain';
import { filterAgentuitySdkKeys, findExistingEnvFile, readEnvFile, splitEnvAndSecrets, } from '../../env-util';
import { ErrorCode, getExitCode } from '../../errors';
import { pauseStepUI, runSteps, StepInterruptError, stepError, stepSkipped, stepSuccess, } from '../../steps';
import * as tui from '../../tui';
import { createSubcommand, DeployOptionsSchema } from '../../types';
import { validateAptDependencies } from '../../utils/apt-validator';
import { extractDependencies } from '../../utils/deps';
import { zipDir } from '../../utils/zip';
import { typecheck } from '../build/typecheck';
import { viteBundle } from '../build/vite-bundler';
import { getProjectGithubStatus } from '../git/api';
import { runGitLink } from '../git/link';
import { runForkedDeploy } from './deploy-fork';
const DeploymentCancelledError = StructuredError('DeploymentCancelled', 'Deployment cancelled by user');
const DeployResponseSchema = z.object({
    success: z.boolean().describe('Whether deployment succeeded'),
    deploymentId: z.string().describe('Deployment ID'),
    projectId: z.string().describe('Project ID'),
    logs: z.array(z.string()).optional().describe('The deployment startup logs'),
    urls: z
        .object({
        deployment: z.string().describe('Deployment-specific URL'),
        latest: z.string().describe('Latest/active deployment URL'),
        custom: z.array(z.string()).optional().describe('Custom domain URLs'),
        dashboard: z.string().describe('The dashboard URL for the deployment'),
    })
        .optional()
        .describe('Deployment URLs'),
});
/**
 * Render the final "Uploaded N assets ..." line after a successful CDN
 * upload pass. Reports both the original and on-the-wire totals when gzip
 * compressed at least one file, so the user can see the real transfer cost.
 */
function formatUploadSummary(count, rawBytes, transferredBytes) {
    const noun = tui.plural(count, 'asset', 'assets');
    // When no compression happened (icons, fonts, binaries), rawBytes ===
    // transferredBytes and the extra detail would just be noise.
    if (transferredBytes === rawBytes) {
        return `✓ Uploaded ${count} ${noun} (${tui.formatBytes(rawBytes)}) to CDN`;
    }
    return `✓ Uploaded ${count} ${noun} (${tui.formatBytes(transferredBytes)} on wire, ${tui.formatBytes(rawBytes)} raw) to CDN`;
}
export const deploySubcommand = createSubcommand({
    name: 'deploy',
    description: 'Deploy project to the Agentuity Cloud',
    tags: [
        'mutating',
        'creates-resource',
        'slow',
        'api-intensive',
        'requires-auth',
        'requires-project',
    ],
    examples: [
        { command: getCommand('cloud deploy'), description: 'Deploy current project' },
        {
            command: getCommand('cloud deploy --log-level=debug'),
            description: 'Deploy with verbose output',
        },
    ],
    toplevel: true,
    idempotent: false,
    requires: { auth: true, project: true, apiClient: true },
    prerequisites: ['auth login'],
    resourceRules: [
        {
            resource: 'project',
            required: true,
            flag: 'project-id',
            envVar: 'AGENTUITY_CLOUD_PROJECT_ID',
            impliedFrom: 'agentuity.json',
        },
        {
            resource: 'region',
            required: true,
            flag: 'region',
            envVar: 'AGENTUITY_REGION',
            configPref: 'region',
            operationType: 'mutate',
        },
    ],
    schema: {
        options: z.intersection(DeployOptionsSchema, z.object({
            reportFile: z
                .string()
                .optional()
                .describe('file path to save build report JSON with errors, warnings, and diagnostics'),
            childMode: z
                .boolean()
                .optional()
                .default(false)
                .describe('Internal: run as forked child process'),
            confirm: z
                .boolean()
                .optional()
                .default(false)
                .describe('Confirm region change without prompting (for non-TTY environments)'),
        })),
        response: DeployResponseSchema,
    },
    async handler(ctx) {
        let { project } = ctx;
        const { apiClient, projectDir, config, options, logger, opts, auth } = ctx;
        // Verify project access and offer import if needed
        const { reconcileProject } = await import('../project/reconcile');
        const { isTTY } = await import('../../auth');
        const reconcileResult = await reconcileProject({
            dir: projectDir,
            auth,
            apiClient,
            config: config,
            logger,
            interactive: isTTY(),
        });
        if (reconcileResult.status === 'error') {
            tui.fatal(reconcileResult.message, ErrorCode.PROJECT_NOT_FOUND);
        }
        if (reconcileResult.status === 'skipped') {
            tui.fatal('Project must be registered with Agentuity Cloud to deploy.', ErrorCode.PROJECT_NOT_FOUND);
        }
        if (reconcileResult.status === 'imported' && reconcileResult.project) {
            // Project was imported - use the new project config
            project = reconcileResult.project;
            tui.newline();
        }
        // Check if local region differs from server region and handle confirmation
        const hasTTY = process.stdin.isTTY && process.stdout.isTTY;
        if (project.region) {
            try {
                // Check cache first to avoid duplicate API calls
                const profile = config?.name ?? 'default';
                let serverProject = getCachedProject(profile, project.projectId);
                if (!serverProject) {
                    serverProject = await projectGet(apiClient, {
                        id: project.projectId,
                        keys: false,
                    });
                    setCachedProject(profile, project.projectId, serverProject);
                }
                const serverRegion = serverProject.cloudRegion;
                if (serverRegion && serverRegion !== project.region) {
                    logger.debug('Region mismatch detected: local=%s, server=%s', project.region, serverRegion);
                    if (hasTTY) {
                        // Interactive mode: prompt for confirmation
                        tui.newline();
                        tui.warning(`Region change detected: ${tui.bold(serverRegion)} → ${tui.bold(project.region)}`);
                        const confirmChange = await tui.confirm('Do you want to update the project region?', false);
                        if (!confirmChange) {
                            tui.newline();
                            tui.fatal('Deployment cancelled. Update the region in agentuity.json or keep the current region.', ErrorCode.CONFIG_INVALID);
                        }
                    }
                    else {
                        // Non-interactive mode: require --confirm flag
                        if (!opts.confirm) {
                            tui.fatal(`Region change detected (${serverRegion} → ${project.region}). Use --confirm flag to proceed with region change in non-interactive mode.`, ErrorCode.CONFIG_INVALID);
                        }
                        logger.debug('Region change confirmed via --confirm flag');
                    }
                    // Update the region on the server
                    await tui.spinner({
                        message: 'Updating project region...',
                        type: 'simple',
                        callback: async () => {
                            await projectUpdateRegion(apiClient, project.projectId, project.region);
                        },
                    });
                    tui.success(`Project region updated to ${tui.bold(project.region)}`);
                    tui.newline();
                }
            }
            catch (err) {
                // If it's a fatal error we threw, re-throw it
                if (err instanceof Error && err.message.includes('Region change detected')) {
                    throw err;
                }
                // Log other errors as non-fatal and continue (e.g., network issues fetching project)
                logger.trace('Failed to check project region: %s', err);
            }
        }
        // Initialize build report collector if reportFile is specified
        const collector = new BuildReportCollector();
        if (opts.reportFile) {
            collector.setOutputPath(opts.reportFile);
            collector.enableAutoWrite();
            setGlobalCollector(collector);
        }
        let deployment;
        let build;
        let instructions;
        let complete;
        let statusResult;
        let malwareCheckPromise;
        const logs = [];
        const sdkKey = await loadProjectSDKKey(ctx.logger, ctx.projectDir);
        // Ensure SDK key is present before proceeding
        if (!sdkKey) {
            ctx.logger.fatal('The AGENTUITY_SDK_KEY value not found in the .env file in this folder. Ensure you are inside a valid Agentuity project folder and run "%s" to pull your environment from the cloud.', getCommand('cloud env pull'));
        }
        // Check if we're running as a forked child process
        const isChildProcess = opts.childMode || process.env.AGENTUITY_FORK_PARENT === '1';
        const deploymentEnv = process.env.AGENTUITY_DEPLOYMENT;
        // If not in child mode and no pre-created deployment, run as fork wrapper to capture crashes
        // (CI builds set AGENTUITY_DEPLOYMENT, fork wrapper also sets it for the child)
        if (!isChildProcess && !deploymentEnv) {
            logger.debug('Running deploy as fork wrapper');
            // First, create the deployment to get the ID, publicKey, and stream URL
            const deploymentConfig = project.deployment ?? {};
            // Validate resource configuration before creating deployment
            if (deploymentConfig.resources) {
                const validation = validateResources(deploymentConfig.resources);
                if (!validation.valid) {
                    tui.error('Invalid resource configuration in agentuity.json:');
                    for (const error of validation.errors) {
                        tui.error(`  ${error}`);
                    }
                    tui.fatal('Fix the resource configuration and try again.', ErrorCode.CONFIG_INVALID);
                }
            }
            // Validate apt dependencies before creating deployment
            if (deploymentConfig.dependencies && deploymentConfig.dependencies.length > 0) {
                const aptValidation = await tui.spinner({
                    message: 'Validating apt dependencies...',
                    type: 'simple',
                    callback: async () => {
                        return await validateAptDependencies(deploymentConfig.dependencies, project.region, config, logger);
                    },
                });
                if (aptValidation.invalid.length > 0) {
                    if (options.json) {
                        return {
                            success: false,
                            deploymentId: '',
                            projectId: project.projectId,
                            errors: aptValidation.invalid.map((pkg) => ({
                                type: 'invalid-apt-dependency',
                                package: pkg.package,
                                error: pkg.error,
                                searchUrl: pkg.searchUrl,
                                availableVersions: pkg.availableVersions,
                            })),
                        };
                    }
                    tui.error('Invalid apt dependencies in agentuity.json:');
                    tui.newline();
                    for (const pkg of aptValidation.invalid) {
                        tui.bullet(`${tui.bold(pkg.package)}: ${pkg.error}`);
                        if (pkg.availableVersions && pkg.availableVersions.length > 0) {
                            tui.muted(`    Available versions: ${pkg.availableVersions.join(', ')}`);
                        }
                        tui.muted(`    Search: ${tui.link(pkg.searchUrl)}`);
                    }
                    tui.newline();
                    tui.fatal('Fix the apt dependencies and try again. Search for valid packages at: https://packages.debian.org/stable/', ErrorCode.CONFIG_INVALID);
                }
            }
            const initialDeployment = await projectDeploymentCreate(apiClient, project.projectId, deploymentConfig);
            logger.debug('Created deployment: %s', initialDeployment.id);
            // Build args to pass to child, excluding child-mode specific ones
            const childArgs = [];
            if (opts.logsUrl)
                childArgs.push(`--logs-url=${opts.logsUrl}`);
            if (opts.trigger)
                childArgs.push(`--trigger=${opts.trigger}`);
            if (opts.commitUrl)
                childArgs.push(`--commit-url=${opts.commitUrl}`);
            if (opts.message)
                childArgs.push(`--message=${opts.message}`);
            if (opts.commit)
                childArgs.push(`--commit=${opts.commit}`);
            if (opts.branch)
                childArgs.push(`--branch=${opts.branch}`);
            if (opts.provider)
                childArgs.push(`--provider=${opts.provider}`);
            if (opts.repo)
                childArgs.push(`--repo=${opts.repo}`);
            if (opts.event)
                childArgs.push(`--event=${opts.event}`);
            if (opts.pullRequestNumber)
                childArgs.push(`--pull-request-number=${opts.pullRequestNumber}`);
            if (opts.pullRequestUrl)
                childArgs.push(`--pull-request-url=${opts.pullRequestUrl}`);
            const result = await runForkedDeploy({
                projectDir,
                apiClient,
                logger,
                sdkKey: sdkKey,
                deployment: initialDeployment,
                args: childArgs,
            });
            if (!result.success) {
                const appUrl = getAppBaseURL(process.env.AGENTUITY_REGION ?? config?.name, config?.overrides);
                const deploymentLink = `${appUrl}/projects/${project.projectId}/deployments/${initialDeployment.id}`;
                tui.fatal(`Deployment failed: ${tui.link(deploymentLink, 'Deployment Page')}`, ErrorCode.BUILD_FAILED);
            }
            return {
                success: true,
                deploymentId: initialDeployment.id,
                projectId: project.projectId,
                logs: result.deployResult?.logs,
                urls: result.deployResult?.urls,
            };
        }
        let useExistingDeployment = false;
        if (deploymentEnv) {
            const ExistingDeploymentSchema = z.object({
                id: z.string(),
                orgId: z.string(),
                publicKey: z.string(),
            });
            try {
                const parsed = JSON.parse(deploymentEnv);
                const result = ExistingDeploymentSchema.safeParse(parsed);
                if (result.success) {
                    deployment = result.data;
                    useExistingDeployment = true;
                    logger.debug('Using existing deployment: %s', result.data.id);
                }
                else {
                    const errors = result.error.issues
                        .map((i) => `${i.path.join('.')}: ${i.message}`)
                        .join(', ');
                    logger.fatal(`Invalid AGENTUITY_DEPLOYMENT schema: ${errors}`);
                }
            }
            catch (err) {
                logger.fatal(`Failed to parse AGENTUITY_DEPLOYMENT: ${err}`);
            }
        }
        // Create a unified abort controller for the entire deploy flow
        const deployAbortController = new AbortController();
        const deployAbortHandler = () => {
            deployAbortController.abort();
        };
        process.on('SIGINT', deployAbortHandler);
        // Start malware check async (runs in parallel with build)
        if (deployment) {
            malwareCheckPromise = (async () => {
                try {
                    logger.debug('Starting malware dependency check');
                    const packages = await extractDependencies(projectDir, logger);
                    if (packages.length === 0) {
                        logger.debug('No packages to check for malware');
                        return null;
                    }
                    logger.debug('Checking %d packages for malware', packages.length);
                    // Use Catalyst client directly for malware check (security routes are on Catalyst)
                    const catalystClient = await getGlobalCatalystAPIClient(logger, auth, config?.name, undefined, config);
                    const result = await projectDeploymentMalwareCheck(catalystClient, deployment.id, packages, deployAbortController.signal);
                    logger.debug('Malware check complete: action=%s, flagged=%d', result.action, result.summary.flagged);
                    return result;
                }
                catch (error) {
                    logger.warn('Malware check failed: %s', error);
                    return null;
                }
            })();
        }
        try {
            await saveProjectDir(projectDir);
            // Check GitHub status and prompt for setup if not linked
            // Skip in non-TTY environments (CI, automated runs) to prevent hanging
            if (!useExistingDeployment && !project.skipGitSetup && hasTTY) {
                try {
                    const githubStatus = await getProjectGithubStatus(apiClient, project.projectId);
                    if (githubStatus.linked && githubStatus.autoDeploy) {
                        // GitHub is already set up with auto-deploy, tell user to push instead
                        tui.newline();
                        tui.info(`This project is linked to ${tui.bold(githubStatus.repoFullName ?? 'GitHub')} with automatic deployments enabled.`);
                        tui.newline();
                        tui.info(`Push a commit to the ${tui.bold(githubStatus.branch ?? 'main')} branch to trigger a deployment.`);
                        tui.newline();
                        throw new DeploymentCancelledError();
                    }
                    if (!githubStatus.linked) {
                        tui.newline();
                        const wantSetup = await tui.confirm('Would you like to set up automatic deployments from GitHub?');
                        if (wantSetup) {
                            const result = await runGitLink({
                                apiClient,
                                projectId: project.projectId,
                                logger,
                                skipAlreadyLinkedCheck: true,
                                config,
                            });
                            if (result.linked && result.autoDeploy) {
                                // GitHub linked with auto-deploy, tell user to push instead
                                tui.newline();
                                tui.info('GitHub integration set up successfully!');
                                tui.newline();
                                tui.info('Push a commit to trigger your first deployment.');
                                tui.newline();
                                throw new DeploymentCancelledError();
                            }
                            if (result.linked) {
                                // Linked but auto-deploy disabled, continue with manual deploy
                                tui.newline();
                                tui.info('GitHub repository linked. Continuing with deployment...');
                                tui.newline();
                            }
                        }
                        else {
                            await updateProjectConfig(projectDir, { skipGitSetup: true }, config);
                            tui.newline();
                            tui.info(`Skipping GitHub setup. Run ${tui.bold(getCommand('git link'))} later to enable it.`);
                            tui.newline();
                        }
                    }
                }
                catch (err) {
                    // Re-throw intentional cancellations
                    if (err instanceof DeploymentCancelledError) {
                        throw err;
                    }
                    // Log other errors as non-fatal and continue
                    logger.trace('Failed to check GitHub status: %s', err);
                }
            }
            await runSteps([
                !project.deployment?.domains?.length
                    ? null
                    : {
                        label: `Validate Custom ${tui.plural(project.deployment.domains.length, 'Domain', 'Domains')}`,
                        run: async () => {
                            if (project.deployment?.domains?.length) {
                                try {
                                    await domain.promptForDNS(project.projectId, project.deployment.domains, project.region, config, () => pauseStepUI(true));
                                    return stepSuccess();
                                }
                                catch (ex) {
                                    return stepError(String(ex), ex);
                                }
                            }
                            return stepSkipped();
                        },
                    },
                {
                    label: 'Sync Env & Secrets',
                    run: async () => {
                        try {
                            const isCIBuild = useExistingDeployment && process.env.AGENTUITY_FORK_PARENT !== '1';
                            if (isCIBuild) {
                                return stepSkipped('skipped in CI build');
                            }
                            // Read env file
                            const envFilePath = await findExistingEnvFile(projectDir);
                            const localEnv = await readEnvFile(envFilePath);
                            // Filter out AGENTUITY_ keys
                            const filteredEnv = filterAgentuitySdkKeys(localEnv);
                            if (Object.keys(filteredEnv).length === 0) {
                                return stepSkipped('no variables to sync');
                            }
                            // Split into env and secrets
                            const { env, secrets } = splitEnvAndSecrets(filteredEnv);
                            if (Object.keys(env).length === 0 && Object.keys(secrets).length === 0) {
                                return stepSkipped('no variables to sync');
                            }
                            // Push to cloud
                            await projectEnvUpdate(apiClient, {
                                id: project.projectId,
                                env,
                                secrets,
                            });
                            return stepSuccess();
                        }
                        catch (ex) {
                            // Non-fatal: log warning but continue deployment
                            const _ex = ex;
                            return stepSkipped(_ex.message ?? 'failed to sync env variables');
                        }
                    },
                },
                {
                    label: 'Build, Verify and Package',
                    run: async (stepCtx) => {
                        if (!deployment) {
                            return stepError('deployment was null');
                        }
                        let capturedOutput = [];
                        const rootDir = resolve(projectDir);
                        // Run typecheck with collector for error reporting
                        const endTypecheckDiagnostic = collector.startDiagnostic('typecheck');
                        const started = Date.now();
                        const typeResult = await typecheck(rootDir, { collector });
                        endTypecheckDiagnostic();
                        if (typeResult.success) {
                            capturedOutput.push(tui.muted(`✓ Typechecked in ${Date.now() - started}ms`));
                        }
                        else {
                            // Errors already added to collector by typecheck()
                            // Write report before returning error
                            if (opts.reportFile) {
                                await collector.forceWrite();
                            }
                            return stepError('Typecheck failed\n\n' + typeResult.output);
                        }
                        try {
                            const bundleResult = await viteBundle({
                                rootDir,
                                dev: false,
                                deploymentId: deployment.id,
                                orgId: deployment.orgId,
                                projectId: project.projectId,
                                region: project.region,
                                logger: ctx.logger,
                                deploymentOptions: opts,
                                deploymentConfig: project.deployment,
                                collector,
                            });
                            capturedOutput = [...capturedOutput, ...bundleResult.output];
                            build = await loadBuildMetadata(join(projectDir, '.agentuity'));
                            instructions = await projectDeploymentUpdate(apiClient, deployment.id, build, stepCtx.signal);
                            return stepSuccess(capturedOutput.length > 0 ? capturedOutput : undefined);
                        }
                        catch (ex) {
                            const _ex = ex;
                            // Write report before returning error
                            if (opts.reportFile) {
                                await collector.forceWrite();
                            }
                            return stepError(_ex.message ?? 'Error building your project', _ex, capturedOutput.length > 0 ? capturedOutput : undefined);
                        }
                    },
                },
                {
                    label: 'Security Scan',
                    run: async () => {
                        if (!malwareCheckPromise) {
                            return stepSkipped('malware check not started');
                        }
                        const result = await malwareCheckPromise;
                        if (!result) {
                            return stepSkipped('malware check unavailable');
                        }
                        if (result.action === 'block' && result.findings.length > 0) {
                            if (opts.reportFile) {
                                for (const finding of result.findings) {
                                    collector.addGeneralError('deploy', `Malicious package: ${finding.name}@${finding.version} (${finding.reason})`);
                                }
                                await collector.forceWrite();
                            }
                            const packageList = result.findings
                                .map((f) => `• ${f.name}@${f.version} (${f.reason})`)
                                .join('\n');
                            // Pause step UI to cleanly render error box
                            pauseStepUI(true);
                            tui.newline();
                            tui.errorBox('Malicious Packages Detected', `Your deployment was blocked because it contains known malicious packages:\n\n${packageList}\n\nRemove these packages from your project and try again.`);
                            tui.newline();
                            process.exit(getExitCode(ErrorCode.MALWARE_DETECTED));
                        }
                        return stepSuccess([`Scanned ${result.summary.scanned} packages`]);
                    },
                },
                {
                    label: 'Encrypt and Upload Deployment',
                    run: async (stepCtx) => {
                        const progress = stepCtx.progress;
                        if (!deployment) {
                            return stepError('deployment was null');
                        }
                        if (!instructions) {
                            return stepError('deployment instructions were null');
                        }
                        // Start diagnostic for zip/encrypt phase
                        const endZipDiagnostic = collector.startDiagnostic('zip-package');
                        progress(5);
                        ctx.logger.trace('Starting deployment zip creation');
                        // zip up the assets folder
                        const deploymentZip = join(tmpdir(), `${deployment.id}.zip`);
                        await zipDir(join(projectDir, '.agentuity'), deploymentZip, {
                            filter: (_filename, relative) => {
                                if (relative.startsWith('.vite/')) {
                                    return false;
                                }
                                if (relative.startsWith('workbench-src/')) {
                                    return false;
                                }
                                if (relative.startsWith('workbench/')) {
                                    return false;
                                }
                                // ignore common stuff we never want to include in the zip
                                if (relative.startsWith('.env'))
                                    return false;
                                if (relative.startsWith('.git/'))
                                    return false;
                                if (relative.startsWith('.ssh/'))
                                    return false;
                                if (relative === '.DS_Store')
                                    return false;
                                return true;
                            },
                        });
                        ctx.logger.trace(`Deployment zip created: ${deploymentZip}`);
                        endZipDiagnostic();
                        progress(20);
                        // Encrypt the deployment zip using the public key from deployment
                        const endEncryptDiagnostic = collector.startDiagnostic('encrypt');
                        const encryptedZip = join(tmpdir(), `${deployment.id}.enc.zip`);
                        try {
                            ctx.logger.trace('Creating public key');
                            const publicKey = createPublicKey({
                                key: deployment.publicKey,
                                format: 'pem',
                                type: 'spki',
                            });
                            ctx.logger.trace('Creating read/write streams');
                            const src = createReadStream(deploymentZip);
                            const dst = createWriteStream(encryptedZip);
                            ctx.logger.trace('Starting encryption');
                            // Wait for encryption to complete
                            await encryptFIPSKEMDEMStream(publicKey, src, dst);
                            ctx.logger.trace('Encryption complete');
                            progress(40);
                            ctx.logger.trace('Waiting for stream to finish');
                            // End stream and wait for it to finish writing
                            await new Promise((resolve, reject) => {
                                dst.once('finish', resolve);
                                dst.once('error', reject);
                                dst.end();
                            });
                            ctx.logger.trace('Stream finished');
                            endEncryptDiagnostic();
                            progress(50);
                            // Start code upload diagnostic
                            const endCodeUploadDiagnostic = collector.startDiagnostic('code-upload');
                            ctx.logger.trace(`Uploading deployment to ${instructions.deployment}`);
                            const zipfile = Bun.file(encryptedZip);
                            const fileSize = await zipfile.size;
                            ctx.logger.trace(`Upload file size: ${fileSize} bytes`);
                            const resp = await fetch(instructions.deployment, {
                                method: 'PUT',
                                headers: {
                                    'Content-Type': 'application/zip',
                                    'Content-Length': String(fileSize),
                                },
                                body: zipfile,
                                signal: stepCtx.signal,
                            });
                            ctx.logger.trace(`Upload response: ${resp.status}`);
                            if (!resp.ok) {
                                endCodeUploadDiagnostic();
                                const errorMsg = `Error uploading deployment: ${await resp.text()}`;
                                collector.addGeneralError('deploy', errorMsg, 'DEPLOY002');
                                if (opts.reportFile) {
                                    await collector.forceWrite();
                                }
                                return stepError(errorMsg);
                            }
                            endCodeUploadDiagnostic();
                            progress(70);
                            ctx.logger.trace('Cancelling upload response body');
                            // No response payload is needed for successful uploads.
                            // Cancel to release resources without buffering into memory.
                            await resp.body?.cancel();
                            ctx.logger.trace('Deleting encrypted zip');
                            await zipfile.delete();
                        }
                        finally {
                            ctx.logger.trace('Cleanup');
                            // Cleanup in case of error
                            if (await Bun.file(encryptedZip).exists()) {
                                await Bun.file(encryptedZip).delete();
                            }
                            await Bun.file(deploymentZip).delete();
                        }
                        progress(80);
                        // Track both the raw on-disk size and the actual bytes we put on
                        // the wire. For gzipped assets these differ significantly; reporting
                        // the transferred total gives the user an honest view of CDN cost.
                        let rawBytes = 0;
                        let transferredBytes = 0;
                        if (build?.assets && build.assets.length > 0) {
                            // Start CDN upload diagnostic
                            const endCdnUploadDiagnostic = collector.startDiagnostic('cdn-upload');
                            ctx.logger.trace(`Uploading ${build.assets.length} assets`);
                            if (!instructions.assets) {
                                const errorMsg = 'server did not provide asset upload URLs; upload aborted';
                                collector.addGeneralError('deploy', errorMsg, 'DEPLOY006');
                                if (opts.reportFile) {
                                    await collector.forceWrite();
                                }
                                return stepError(errorMsg);
                            }
                            // Pre-flight: every asset the build emitted must have a signed
                            // PUT URL from the backend. Failing up-front gives a single
                            // clear error instead of aborting mid-batch with partial uploads.
                            for (const asset of build.assets) {
                                if (!instructions.assets[asset.filename]) {
                                    const errorMsg = `server did not provide upload URL for asset "${asset.filename}"; upload aborted`;
                                    collector.addGeneralError('deploy', errorMsg, 'DEPLOY006');
                                    if (opts.reportFile) {
                                        await collector.forceWrite();
                                    }
                                    return stepError(errorMsg);
                                }
                                rawBytes += asset.size;
                            }
                            // Track every temp gzip file we create so we can clean them up
                            // even if the deploy is aborted mid-upload (e.g. Ctrl+C).
                            const tempFiles = new Set();
                            const cleanupTempFiles = () => {
                                for (const p of tempFiles) {
                                    try {
                                        unlinkSync(p);
                                    }
                                    catch {
                                        // ignore — may already be gone
                                    }
                                }
                                tempFiles.clear();
                            };
                            // Hoist narrowed locals for use inside the async upload closure;
                            // TS doesn't propagate narrowings of `build` / `instructions`
                            // across async callbacks, and we've already null-checked both
                            // above.
                            const assets = build.assets;
                            const assetUrls = instructions.assets;
                            try {
                                // Upload each asset with bounded concurrency. gzip compression
                                // runs inside the per-asset task, so compressible files
                                // compress in parallel (up to `concurrency`) rather than
                                // serially — a meaningful win for builds with many JS/CSS
                                // chunks since gzip is single-threaded per call.
                                const uploadOne = async (asset) => {
                                    const assetUrl = assetUrls[asset.filename];
                                    // Asset filename already includes the subdirectory
                                    // (e.g., "client/assets/main-abc123.js").
                                    const filePath = join(projectDir, '.agentuity', asset.filename);
                                    const headers = {
                                        'Content-Type': asset.contentType,
                                    };
                                    let body;
                                    let gzTempPath;
                                    let onWireSize = asset.size;
                                    if (asset.contentEncoding === 'gzip') {
                                        // Gzip to a temp file so Bun.file() can provide
                                        // Content-Length to S3 (streaming bodies use chunked
                                        // transfer encoding which S3 rejects).
                                        gzTempPath = join(tmpdir(), `agentuity-asset-${deployment.id}-${Date.now()}-${asset.filename.replace(/\//g, '_')}.gz`);
                                        tempFiles.add(gzTempPath);
                                        await pipeline(createReadStream(filePath), createGzip(), createWriteStream(gzTempPath));
                                        headers['Content-Encoding'] = 'gzip';
                                        body = Bun.file(gzTempPath);
                                        onWireSize = body.size;
                                        ctx.logger.trace(`Gzip compressed ${asset.filename} (${asset.size} -> ${onWireSize} bytes)`);
                                    }
                                    else {
                                        body = Bun.file(filePath);
                                    }
                                    const response = await fetch(assetUrl, {
                                        method: 'PUT',
                                        headers,
                                        body,
                                        signal: stepCtx.signal,
                                    });
                                    if (gzTempPath) {
                                        try {
                                            unlinkSync(gzTempPath);
                                        }
                                        catch {
                                            // ignore
                                        }
                                        tempFiles.delete(gzTempPath);
                                    }
                                    if (!response.ok) {
                                        throw new Error(`asset "${asset.filename}" upload failed: ${response.status} ${await response.text()}`);
                                    }
                                    transferredBytes += onWireSize;
                                };
                                const concurrency = Math.min(4, assets.length);
                                for (let i = 0; i < assets.length; i += concurrency) {
                                    const batch = assets.slice(i, i + concurrency);
                                    await Promise.all(batch.map(uploadOne));
                                }
                            }
                            catch (error) {
                                cleanupTempFiles();
                                const errorMsg = error instanceof Error ? error.message : String(error);
                                collector.addGeneralError('deploy', errorMsg, 'DEPLOY006');
                                if (opts.reportFile) {
                                    await collector.forceWrite();
                                }
                                return stepError(errorMsg);
                            }
                            ctx.logger.trace(`Asset uploads complete: ${build.assets.length} files, raw=${rawBytes}B, on-wire=${transferredBytes}B`);
                            endCdnUploadDiagnostic();
                            progress(95);
                        }
                        else {
                            ctx.logger.debug('No assets to upload to CDN');
                        }
                        progress(100);
                        const output = build?.assets.length
                            ? [
                                tui.muted(formatUploadSummary(build.assets.length, rawBytes, transferredBytes)),
                            ]
                            : undefined;
                        return stepSuccess(output);
                    },
                },
                {
                    label: 'Provision Deployment',
                    run: async (stepCtx) => {
                        if (!deployment) {
                            return stepError('deployment was null');
                        }
                        complete = await projectDeploymentComplete(apiClient, deployment.id, stepCtx.signal);
                        return stepSuccess();
                    },
                },
            ].filter(Boolean), options.logLevel);
            if (!deployment) {
                return {
                    success: false,
                    deploymentId: '',
                    projectId: project.projectId,
                };
            }
            // TODO: send the deployment failure to the backend otherwise we staying in a deploying state
            const streamId = complete?.streamId;
            const appUrl = getAppBaseURL(process.env.AGENTUITY_REGION ?? config?.name, config?.overrides);
            const dashboard = `${appUrl}/r/${deployment.id}`;
            // Poll for deployment status with optional log streaming
            const endDeploymentWaitDiagnostic = collector.startDiagnostic('deployment-wait');
            const pollInterval = 500;
            const maxAttempts = 600;
            let attempts = 0;
            // Reuse the deploy abort controller for polling (already aborted on Ctrl+C)
            try {
                if (streamId) {
                    // Use progress logger to stream logs while polling
                    const streamsUrl = getStreamURL(project.region, config);
                    await tui
                        .progress({
                        message: 'Deploying project...',
                        type: 'logger',
                        maxLines: 2,
                        clearOnSuccess: true,
                        callback: async (log) => {
                            // Start log streaming
                            const logStreamController = new AbortController();
                            const logStreamPromise = (async () => {
                                try {
                                    logger.debug('fetching stream: %s/%s', streamsUrl, streamId);
                                    const resp = await fetch(`${streamsUrl}/${streamId}`, {
                                        signal: logStreamController.signal,
                                        headers: {
                                            Authorization: `Bearer ${sdkKey}`,
                                            'User-Agent': getUserAgent(),
                                        },
                                    });
                                    if (!resp.ok || !resp.body) {
                                        ctx.logger.trace(`Failed to connect to warmup log stream: ${resp.status}`);
                                        return;
                                    }
                                    const reader = resp.body.getReader();
                                    const decoder = new TextDecoder();
                                    let buffer = '';
                                    while (true) {
                                        const { done, value } = await reader.read();
                                        if (done)
                                            break;
                                        buffer += decoder.decode(value, { stream: true });
                                        const lines = buffer.split('\n');
                                        buffer = lines.pop() || ''; // Keep incomplete line in buffer
                                        for (const line of lines) {
                                            // Strip ISO 8601 timestamp prefix if present
                                            const message = line.replace(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\s?/, '');
                                            if (message) {
                                                logs.push(message);
                                                log(message);
                                            }
                                        }
                                    }
                                }
                                catch (err) {
                                    if (err instanceof Error && err.name === 'AbortError') {
                                        return;
                                    }
                                    ctx.logger.trace(`Warmup log stream error: ${err}`);
                                }
                            })();
                            // Poll for deployment status
                            while (attempts < maxAttempts) {
                                // Check if user pressed Ctrl+C
                                if (deployAbortController.signal.aborted) {
                                    logStreamController.abort();
                                    throw new DeploymentCancelledError();
                                }
                                attempts++;
                                try {
                                    statusResult = await projectDeploymentStatus(apiClient, deployment?.id ?? '', deployAbortController.signal);
                                    logger.trace('status result: %s', statusResult);
                                    if (statusResult.state === 'completed') {
                                        logStreamController.abort();
                                        break;
                                    }
                                    if (statusResult.state === 'failed') {
                                        throw new Error('Deployment failed');
                                    }
                                    await Bun.sleep(pollInterval);
                                }
                                catch (err) {
                                    logStreamController.abort();
                                    throw err;
                                }
                            }
                            // Wait for log stream to finish
                            await logStreamPromise;
                            if (attempts >= maxAttempts) {
                                throw new Error('Deployment timed out');
                            }
                        },
                    })
                        .then(() => {
                        endDeploymentWaitDiagnostic();
                        tui.success('Your project was deployed!');
                    })
                        .catch(async (ex) => {
                        endDeploymentWaitDiagnostic();
                        // Handle cancellation
                        if (ex instanceof DeploymentCancelledError) {
                            if (opts.reportFile) {
                                await collector.forceWrite();
                            }
                            tui.warning('Deployment cancelled');
                            process.exit(130); // Standard exit code for SIGINT
                        }
                        const exwithmessage = ex;
                        const msg = exwithmessage.message === 'Deployment failed'
                            ? ''
                            : exwithmessage.toString();
                        // Add error to collector
                        const isTimeout = exwithmessage.message === 'Deployment timed out';
                        collector.addGeneralError('deploy', msg || 'Deployment failed', isTimeout ? 'DEPLOY003' : 'DEPLOY004');
                        if (opts.reportFile) {
                            await collector.forceWrite();
                        }
                        tui.error(`Your deployment failed to start${msg ? `: ${msg}` : ''}`);
                        if (logs.length) {
                            const logsDir = join(getDefaultConfigDir(), 'logs');
                            if (!existsSync(logsDir)) {
                                mkdirSync(logsDir, { recursive: true });
                            }
                            const errorFile = join(logsDir, `${deployment?.id ?? Date.now()}.txt`);
                            writeFileSync(errorFile, logs.join('\n'));
                            const count = Math.min(logs.length, 10);
                            const last = logs.length - count;
                            tui.newline();
                            tui.warning(`The last ${count} lines of the log:`);
                            let offset = last + 1; // we want to show the offset from inside the log starting at 1
                            const max = String(logs.length).length;
                            for (const _log of logs.slice(last)) {
                                console.log(tui.muted(`${offset.toFixed().padEnd(max)} | ${_log}`));
                                offset++;
                            }
                            tui.newline();
                            tui.fatal(`The logs were written to ${errorFile}`, ErrorCode.BUILD_FAILED);
                        }
                        tui.fatal('Deployment failed', ErrorCode.BUILD_FAILED);
                    });
                }
                else {
                    // No stream ID - poll without log streaming
                    await tui.spinner({
                        message: 'Deploying project...',
                        type: 'simple',
                        clearOnSuccess: true,
                        callback: async () => {
                            while (attempts < maxAttempts) {
                                // Check if user pressed Ctrl+C
                                if (deployAbortController.signal.aborted) {
                                    throw new DeploymentCancelledError();
                                }
                                attempts++;
                                statusResult = await projectDeploymentStatus(apiClient, deployment?.id ?? '', deployAbortController.signal);
                                if (statusResult.state === 'completed') {
                                    break;
                                }
                                if (statusResult.state === 'failed') {
                                    throw new Error('Deployment failed');
                                }
                                await Bun.sleep(pollInterval);
                            }
                            if (attempts >= maxAttempts) {
                                throw new Error('Deployment timed out');
                            }
                        },
                    });
                    endDeploymentWaitDiagnostic();
                    tui.success('Your project was deployed!');
                }
            }
            catch (ex) {
                endDeploymentWaitDiagnostic();
                const exwithmessage = ex;
                const isTimeout = exwithmessage?.message === 'Deployment timed out';
                collector.addGeneralError('deploy', exwithmessage?.message || String(ex), isTimeout ? 'DEPLOY003' : 'DEPLOY004');
                if (opts.reportFile) {
                    await collector.forceWrite();
                }
                const lines = [`${ex}`, ''];
                lines.push(`${tui.ICONS.arrow} ${tui.bold(tui.padRight('Dashboard:', 12)) + tui.link(dashboard)}`);
                tui.banner(tui.colorError(`Deployment: ${deployment.id} Failed`), lines.join('\n'), {
                    centerTitle: false,
                    topSpacer: false,
                    bottomSpacer: false,
                });
                tui.fatal('Deployment failed', ErrorCode.BUILD_FAILED);
            }
            finally {
                // Clean up signal handler
                process.off('SIGINT', deployAbortHandler);
            }
            // Show deployment URLs
            if (complete?.publicUrls && !options.json) {
                const lines = [];
                if (complete.publicUrls.custom?.length) {
                    for (const url of complete.publicUrls.custom) {
                        lines.push(`${tui.ICONS.arrow} ${tui.bold(tui.padRight('Deployment:', 12)) + tui.link(url)}`);
                    }
                }
                else {
                    // Prefer vanity URLs, fall back to hash-based
                    const deploymentUrl = complete.publicUrls.vanityDeployment ?? complete.publicUrls.deployment;
                    const latestUrl = complete.publicUrls.vanityProject ?? complete.publicUrls.latest;
                    lines.push(`${tui.ICONS.arrow} ${tui.bold(tui.padRight('Deployment:', 12)) + tui.link(deploymentUrl)}`);
                    lines.push(`${tui.ICONS.arrow} ${tui.bold(tui.padRight('Project:', 12)) + tui.link(latestUrl)}`);
                }
                lines.push(`${tui.ICONS.arrow} ${tui.bold(tui.padRight('Dashboard:', 12)) + tui.link(dashboard)}`);
                tui.banner(`Deployment: ${tui.colorPrimary(deployment.id)}`, lines.join('\n'), {
                    centerTitle: false,
                    topSpacer: false,
                    bottomSpacer: false,
                });
            }
            // Trigger TLS certificate provisioning for custom domains (fire-and-forget)
            if (project.deployment?.domains?.length) {
                void domain.triggerTLSProvisioning(project.deployment.domains);
            }
            // Write final report on success
            if (opts.reportFile) {
                await collector.forceWrite();
            }
            clearGlobalCollector();
            // Write deploy result to file for fork parent to consume
            const deployResultFile = process.env.AGENTUITY_DEPLOY_RESULT_FILE;
            if (deployResultFile) {
                try {
                    const resultData = {
                        urls: complete?.publicUrls
                            ? {
                                deployment: complete.publicUrls.vanityDeployment ??
                                    complete.publicUrls.deployment,
                                latest: complete.publicUrls.vanityProject ?? complete.publicUrls.latest,
                                custom: complete.publicUrls.custom,
                                dashboard,
                            }
                            : undefined,
                        logs,
                    };
                    writeFileSync(deployResultFile, JSON.stringify(resultData));
                }
                catch {
                    // Non-fatal: result file is optional
                }
            }
            return {
                success: true,
                deploymentId: deployment.id,
                projectId: project.projectId,
                logs,
                urls: complete?.publicUrls
                    ? {
                        deployment: complete.publicUrls.vanityDeployment ?? complete.publicUrls.deployment,
                        latest: complete.publicUrls.vanityProject ?? complete.publicUrls.latest,
                        custom: complete.publicUrls.custom,
                        dashboard,
                    }
                    : undefined,
            };
        }
        catch (ex) {
            // Handle step interruption (Ctrl+C during build steps)
            if (ex instanceof StepInterruptError) {
                tui.warning('Deployment cancelled');
                process.exit(ex.exitCode);
            }
            collector.addGeneralError('deploy', String(ex), 'DEPLOY004');
            if (opts.reportFile) {
                await collector.forceWrite();
            }
            clearGlobalCollector();
            tui.fatal(`unexpected error trying to deploy project. ${ex}`);
        }
        finally {
            process.off('SIGINT', deployAbortHandler);
        }
    },
});
//# sourceMappingURL=deploy.js.map