/**
 * Offline / pack-only deploy mode
 * -------------------------------
 *
 * Builds the project the same way a real deploy would, zips the staging
 * tree with the production deploy filter, then optionally writes the zip
 * to disk and/or PUTs it to a caller-provided URL (e.g. a presigned S3
 * URL). No cloud deployment is created, no encryption, no Agentuity API
 * calls, and no agentuity.json / login / DNS requirements.
 *
 *   agentuity deploy --pack-only --log-level=trace
 *   agentuity deploy --upload-url 'https://bucket.s3.../presigned'
 *   agentuity deploy --pack-only --upload-url '...' --pack-output ./out.zip
 */
import { createReadStream, mkdirSync, statSync } from 'node:fs';
import { rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
import { Readable } from 'node:stream';
import { clearGlobalCollector } from "../../../build-report.js";
import * as tui from "../../../tui.js";
import { PACK_ONLY_DEPLOYMENT_ID } from "../../build/adapters/cdn-origin.js";
import { runBuildPipeline } from "../../build/run.js";
import { DEPLOY_PACK_ZIP_BASENAME, packageDeploymentZip } from "./package.js";
/** Stub project fields used when offline deploy has no explicit identity. */
export const OFFLINE_DEPLOY_PROJECT_ID = 'offline';
export const OFFLINE_DEPLOY_ORG_ID = 'offline';
export const OFFLINE_DEPLOY_REGION = 'local';
/**
 * Resolve project/org/region for offline pack metadata.
 *
 * Precedence (first non-empty wins per field):
 * 1. Explicit CLI flags / params (`--project-id`, `--org-id`, `--region`)
 * 2. Soft-loaded / optional project object (if present)
 * 3. Offline stubs (`offline` / `local`)
 */
export function resolveOfflineDeployIdentity(params) {
    const pick = (value) => {
        if (typeof value !== 'string')
            return undefined;
        const trimmed = value.trim();
        return trimmed.length > 0 ? trimmed : undefined;
    };
    return {
        projectId: pick(params.projectId) ?? pick(params.project?.projectId) ?? OFFLINE_DEPLOY_PROJECT_ID,
        orgId: pick(params.orgId) ?? pick(params.project?.orgId) ?? OFFLINE_DEPLOY_ORG_ID,
        region: pick(params.region) ?? pick(params.project?.region) ?? OFFLINE_DEPLOY_REGION,
    };
}
/**
 * Resolve where the pack-only zip should land.
 * Default: `<projectDir>/agentuity-deploy.zip`.
 */
export function resolvePackOutputPath(projectDir, packOutput) {
    if (packOutput) {
        return isAbsolute(packOutput) ? packOutput : resolve(projectDir, packOutput);
    }
    return join(projectDir, DEPLOY_PACK_ZIP_BASENAME);
}
/**
 * Resolve pack-only zip path, refusing to write inside the staging tree
 * (which would race the zip scan). Exported for unit tests.
 */
export function resolveSafePackOutput(stagingDir, requested, logger, json) {
    const stagingResolved = resolve(stagingDir);
    const outputResolved = resolve(requested);
    if (outputResolved === stagingResolved ||
        outputResolved.startsWith(`${stagingResolved}/`) ||
        outputResolved.startsWith(`${stagingResolved}\\`)) {
        const fallback = join(dirname(stagingResolved), DEPLOY_PACK_ZIP_BASENAME);
        logger.debug('Pack output path is inside staging dir; writing to %s instead', fallback);
        if (!json) {
            tui.warning(`Pack output was inside the staging directory; writing to ${fallback} instead`);
        }
        return fallback;
    }
    return outputResolved;
}
/**
 * PUT a local zip to a presigned/one-time URL (plain zip, no encryption).
 * Exported for unit tests.
 */
export async function uploadDeploymentZip(params) {
    const { zipPath, uploadUrl, logger, signal } = params;
    const fileSize = statSync(zipPath).size;
    logger.debug('Uploading deployment zip (%d bytes) to provided URL', fileSize);
    logger.trace('Upload URL host: %s', (() => {
        try {
            return new URL(uploadUrl).host;
        }
        catch {
            return '(invalid url)';
        }
    })());
    const body = Readable.toWeb(createReadStream(zipPath));
    const resp = await fetch(uploadUrl, {
        method: 'PUT',
        headers: {
            'Content-Type': 'application/zip',
            'Content-Length': String(fileSize),
        },
        body,
        signal,
        duplex: 'half',
    });
    logger.trace('Upload response status: %d', resp.status);
    if (!resp.ok) {
        const text = await resp.text().catch(() => '');
        throw new Error(`Upload to --upload-url failed: HTTP ${resp.status}${text ? ` — ${text.slice(0, 500)}` : ''}`);
    }
    // No response body needed; cancel to free the connection promptly.
    await resp.body?.cancel().catch(() => undefined);
}
/**
 * Build + package without Agentuity cloud. Optionally writes a local zip
 * and/or PUTs it to `--upload-url`. Flushes the build report collector
 * when `--report-file` is set (success or failure).
 */
export async function runPackOnly(params) {
    const { project, projectDir, logger, collector, deployOptions, hasReportFile, packOutput, uploadUrl, json, } = params;
    const rootDir = resolve(projectDir);
    const { projectId, orgId, region } = resolveOfflineDeployIdentity({
        projectId: params.projectId,
        orgId: params.orgId,
        region: params.region,
        project,
    });
    logger.debug('Offline deploy identity: projectId=%s orgId=%s region=%s', projectId, orgId, region);
    // Retain a local zip when the user asked for --pack-only, or passed an
    // explicit --pack-output. With only --upload-url, use a temp file and
    // delete it after a successful upload.
    const retainLocalZip = Boolean(deployOptions.packOnly) || Boolean(packOutput);
    const useTempZip = Boolean(uploadUrl) && !retainLocalZip;
    const requestedOutput = useTempZip
        ? join(tmpdir(), `agentuity-offline-${Date.now()}-${DEPLOY_PACK_ZIP_BASENAME}`)
        : resolvePackOutputPath(rootDir, packOutput);
    const modeLabel = uploadUrl
        ? retainLocalZip
            ? 'Offline pack + upload'
            : 'Offline upload'
        : 'Pack-only';
    logger.debug('%s mode: building and packaging without Agentuity cloud', modeLabel);
    if (!json) {
        tui.info(uploadUrl
            ? `${modeLabel} — building, packaging, and uploading (no login / agentuity.json required)`
            : 'Pack-only mode — building and packaging without uploading (no login / agentuity.json required)');
    }
    let zipPathForCleanup;
    try {
        const pipelineResult = await runBuildPipeline({
            projectDir: rootDir,
            logger,
            collector,
            projectId,
            orgId,
            region,
            deploymentId: PACK_ONLY_DEPLOYMENT_ID,
            deploymentOptions: deployOptions,
            deploymentConfig: project?.deployment,
            skipTypeCheck: deployOptions.skipTypeCheck,
        });
        const { framework, monorepo, buildResult, outputDir } = pipelineResult;
        const logs = [...pipelineResult.logs];
        // Structured flag from monorepo staging; monorepo fallback keeps the
        // staging-vs-zip tip useful even when no ignore file was present.
        const usedIgnorePatterns = Boolean(pipelineResult.usedIgnorePatterns ?? buildResult.usedIgnorePatterns);
        const frameworkLabel = framework.version
            ? `${framework.name} v${framework.version}`
            : framework.name;
        if (!json) {
            tui.success(`Detected ${tui.bold(frameworkLabel)} (${framework.runtime})`);
            if (monorepo) {
                tui.info(`Workspace ${tui.muted(monorepo.root)} (subpackage: ${tui.bold(monorepo.subpath)})`);
            }
            for (const line of buildResult.logs) {
                tui.info(tui.muted(line));
            }
        }
        const stagingDir = buildResult.outputDir ?? outputDir;
        const finalOutput = resolveSafePackOutput(stagingDir, requestedOutput, logger, json);
        mkdirSync(dirname(finalOutput), { recursive: true });
        const zipResult = await packageDeploymentZip({
            sourceDir: stagingDir,
            outputPath: finalOutput,
            logger,
        });
        if (useTempZip) {
            zipPathForCleanup = zipResult.outputPath;
        }
        const relPack = relative(rootDir, zipResult.outputPath) || zipResult.outputPath;
        if (!json) {
            tui.success(`Packaged ${zipResult.added} file(s) (${tui.formatBytes(zipResult.sizeBytes)})${retainLocalZip || !uploadUrl ? ` → ${tui.bold(relPack)}` : ''}`);
            if (zipResult.skipped > 0) {
                tui.info(tui.muted(`${zipResult.skipped} staging path(s) not packed (zip filter / symlink / directory)`));
            }
            if (usedIgnorePatterns || monorepo) {
                tui.info(tui.muted('Ignore exclusions happen during monorepo staging (see "Excluded … via .agentuityignore" above), not in the zip step.'));
            }
            if (retainLocalZip || !uploadUrl) {
                tui.info(tui.muted(`Inspect with: unzip -l ${relPack}  ·  re-run with --log-level=trace to list every staged entry`));
            }
        }
        logs.push(`Packaged ${zipResult.added} file(s) from staging (${zipResult.skipped} not packed at zip step), ${zipResult.sizeBytes} bytes → ${zipResult.outputPath}`);
        let uploaded = false;
        if (uploadUrl) {
            if (!json) {
                tui.info('Uploading deployment zip to provided URL...');
            }
            await uploadDeploymentZip({
                zipPath: zipResult.outputPath,
                uploadUrl,
                logger,
            });
            uploaded = true;
            logs.push(`Uploaded ${zipResult.sizeBytes} bytes to --upload-url`);
            if (!json) {
                tui.success(`Uploaded ${tui.formatBytes(zipResult.sizeBytes)} to the provided upload URL`);
            }
        }
        return {
            success: true,
            projectId,
            packPath: retainLocalZip || !uploadUrl ? zipResult.outputPath : undefined,
            fileCount: zipResult.added,
            skippedCount: zipResult.skipped,
            sizeBytes: zipResult.sizeBytes,
            stagingDir,
            usedIgnorePatterns: usedIgnorePatterns || Boolean(monorepo),
            uploaded: uploaded || undefined,
            logs,
        };
    }
    finally {
        if (zipPathForCleanup) {
            await rm(zipPathForCleanup, { force: true }).catch(() => undefined);
        }
        if (hasReportFile) {
            await collector.forceWrite();
            clearGlobalCollector();
        }
    }
}
//# sourceMappingURL=pack.js.map