"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@oclif/core");
const chalk_1 = __importDefault(require("chalk"));
const ora_1 = __importDefault(require("ora"));
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const node_child_process_1 = require("node:child_process");
const api_client_1 = require("../lib/api-client");
const config_storage_1 = require("../lib/config-storage");
const resolve_cluster_1 = require("../lib/resolve-cluster");
const framework_detector_1 = require("../lib/framework-detector");
const framework_postchecks_1 = require("../lib/framework-postchecks");
const POLL_INTERVAL_MS = 5000;
const MAX_WAIT_CATALOG_MS = 600_000; // 10 min for catalog installs
const MAX_WAIT_SOURCE_MS = 2_100_000; // 35 min for source builds (GH Actions + deploy)
// ── CLI command ────────────────────────────────────────────────────────────
class Deploy extends core_1.Command {
    async run() {
        const { args, flags } = await this.parse(Deploy);
        const filePath = node_path_1.default.resolve(args.file ?? 'flui.yaml');
        if (!node_fs_1.default.existsSync(filePath)) {
            this.error(`Manifest not found: ${filePath}\n  Create one with \`flui init\` or point to an existing file.`, { exit: 1 });
        }
        const raw = node_fs_1.default.readFileSync(filePath, 'utf-8');
        const configStorage = new config_storage_1.ConfigStorage();
        const apiUrl = configStorage.getApiUrlOrThrow();
        const apiKey = configStorage.getApiKey();
        if (!apiKey) {
            this.error('Not logged in. Run `flui auth login` first.', { exit: 1 });
        }
        const apiClient = new api_client_1.ApiClient({ baseUrl: apiUrl, apiKey: apiKey });
        // Detect manifest kind
        const kind = this.detectKind(raw);
        if (flags['validate-only']) {
            await this.validateManifest(apiClient, raw);
            return;
        }
        if (kind === 'Application') {
            await this.runSourceDeploy(apiClient, raw, flags, filePath);
        }
        else {
            await this.runCatalogDeploy(apiClient, raw, flags);
        }
    }
    // ── Source deploy (kind: Application) ─────────────────────────────────────
    async runSourceDeploy(apiClient, raw, flags, filePath) {
        let resolved;
        try {
            resolved = await (0, resolve_cluster_1.resolveCluster)(flags.cluster);
        }
        catch (error) {
            this.error(error.message, { exit: 1 });
        }
        const { id: clusterId, name: clusterName } = resolved;
        const repoFullName = flags.repo ??
            this.detectGitRemote(node_path_1.default.dirname(filePath));
        if (!repoFullName) {
            this.error('Could not detect GitHub repository. ' +
                'Provide it with --repo owner/repo or run from inside a git repository with a remote named "origin".', { exit: 1 });
        }
        const branch = flags.branch ??
            this.detectGitBranch(node_path_1.default.dirname(filePath)) ??
            'main';
        const envOverrides = this.parseEnvOverrides(flags.env ?? []);
        const skipBuild = flags['no-build'] || !!flags.image;
        const explicitImage = flags.image;
        if (!skipBuild && !explicitImage) {
            const blocked = this.runPostChecksOrBail(node_path_1.default.dirname(filePath), flags['skip-checks']);
            if (blocked)
                return;
        }
        this.printDeployHeader({
            filePath,
            clusterName,
            repoFullName,
            branch,
            explicitImage,
            skipBuild,
            envOverrides,
        });
        let spinnerLabel;
        if (explicitImage) {
            spinnerLabel = `Deploying ${explicitImage}…`;
        }
        else if (skipBuild) {
            spinnerLabel = 'Re-deploying current image…';
        }
        else {
            spinnerLabel = 'Submitting manifest…';
        }
        const spinner = (0, ora_1.default)(spinnerLabel).start();
        let deploy;
        try {
            deploy = await apiClient.post('/applications/deploy-from-yaml', {
                yaml: raw,
                clusterId,
                repoFullName,
                branch,
                skipBuild,
                ...(explicitImage ? { imageRef: explicitImage } : {}),
                ...(Object.keys(envOverrides).length > 0 ? { envOverrides } : {}),
            });
            spinner.succeed(skipBuild ? 'Deploy triggered (build skipped)' : 'Build triggered');
        }
        catch (error) {
            spinner.fail('Failed to trigger deploy');
            const msg = error.response?.data?.message ?? error.message;
            console.log(chalk_1.default.red(`\n  Error: ${msg}\n`));
            if (/GitHub integration is not connected/i.test(msg)) {
                console.log(chalk_1.default.yellow('  Hint: run `flui integration connect github` to install the Flui GitHub App.\n'));
            }
            else if (/GHCR PAT/i.test(msg)) {
                console.log(chalk_1.default.yellow('  Hint: run `flui integration ghcr-pat set` to add a GitHub PAT with read:packages scope.\n'));
            }
            else if (/Repository .* is not connected/i.test(msg)) {
                console.log(chalk_1.default.yellow(`  Hint: run \`flui repo connect ${repoFullName}\` to import this repository into Flui.\n`));
            }
            this.exit(1);
        }
        console.log('');
        console.log(`  ${chalk_1.default.bold('App:')}    ${deploy.name} (${deploy.slug})`);
        if (deploy.workflowUrl) {
            console.log(`  ${chalk_1.default.bold('Workflow:')} ${chalk_1.default.dim(deploy.workflowUrl)}`);
        }
        console.log('');
        const detach = flags.detach || flags['no-wait'];
        if (detach) {
            if (deploy.workflowRunUrl) {
                console.log(`  ${chalk_1.default.bold('Build:')} ${chalk_1.default.cyan(deploy.workflowRunUrl)}`);
            }
            console.log(chalk_1.default.dim(`  Track progress:  flui app status ${deploy.slug}\n`));
            return;
        }
        await this.pollSourceDeploy(apiClient, deploy);
    }
    async pollSourceDeploy(apiClient, deploy) {
        console.log(chalk_1.default.dim(`  Waiting for build + deploy to complete (up to ${MAX_WAIT_SOURCE_MS / 60000} min)…`));
        const spinner = (0, ora_1.default)('Waiting for GitHub Actions build…').start();
        const started = Date.now();
        let lastPhase = '';
        while (Date.now() - started < MAX_WAIT_SOURCE_MS) {
            await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
            const done = await this.pollSourceDeployTick(apiClient, deploy, spinner, started, lastPhase);
            if (done.finished) {
                if (done.failed)
                    this.exit(1);
                return;
            }
            lastPhase = done.lastPhase;
        }
        spinner.warn('Timed out waiting for build + deploy');
        console.log(chalk_1.default.yellow(`\n  Still in progress. Track with:`));
        console.log(chalk_1.default.dim(`    flui app status ${deploy.slug}\n`));
    }
    async pollSourceDeployTick(apiClient, deploy, spinner, started, lastPhase) {
        try {
            const app = await apiClient.get(`/applications/${deploy.applicationId}`);
            const elapsed = Math.floor((Date.now() - started) / 1000);
            const phase = this.resolvePhase(app);
            if (this.isRunningStatus(app.status)) {
                spinner.succeed(chalk_1.default.green(`"${deploy.name}" is live`));
                console.log('');
                if (app.resolvedFqdn) {
                    const url = chalk_1.default.cyan(`https://${app.resolvedFqdn}`);
                    console.log(`  ${chalk_1.default.bold('URL:')} ${url}`);
                }
                console.log(chalk_1.default.dim(`  Run \`flui app status ${deploy.slug}\` for details.\n`));
                return { finished: true, lastPhase: phase };
            }
            if (this.isFailedStatus(app.status)) {
                spinner.fail('Deploy failed');
                const msg = app.reconciliationError ?? 'Unknown error';
                console.log(chalk_1.default.red(`\n  Error: ${msg}\n`));
                if (app.workflowRunUrl) {
                    console.log(chalk_1.default.dim(`  Build logs: ${app.workflowRunUrl}\n`));
                }
                return { finished: true, lastPhase: phase, failed: true };
            }
            const buildHint = app.workflowRunUrl && this.isAwaitingBuildStatus(app.status)
                ? chalk_1.default.dim(` → ${app.workflowRunUrl}`)
                : '';
            spinner.text = `${phase} [${elapsed}s]${buildHint}`;
            return { finished: false, lastPhase: phase };
        }
        catch {
            return { finished: false, lastPhase };
        }
    }
    resolvePhase(app) {
        const s = (app.status ?? '').toLowerCase();
        if (s === 'awaiting_build') {
            if (app.lastBuildStatus === 'in_progress')
                return 'Building image via GitHub Actions…';
            if (app.lastBuildStatus === 'completed' &&
                app.lastBuildConclusion !== 'success') {
                return 'Build completed — waiting for result…';
            }
            return 'Waiting for GitHub Actions build…';
        }
        if (s === 'provisioning')
            return 'Deploying to cluster…';
        if (s === 'updating')
            return 'Rolling out update…';
        return `${s}…`;
    }
    /**
     * The API serialises ApplicationStatus values as lowercase
     * (`running`, `failed`, `awaiting_build`, …). Compare normalised so that
     * historical uppercase strings — or future case drift — do not silently
     * keep the polling loop alive forever.
     */
    isRunningStatus(s) {
        return (s ?? '').toLowerCase() === 'running';
    }
    isFailedStatus(s) {
        return (s ?? '').toLowerCase() === 'failed';
    }
    isAwaitingBuildStatus(s) {
        return (s ?? '').toLowerCase() === 'awaiting_build';
    }
    // ── Catalog deploy (kind: CatalogApp) ─────────────────────────────────────
    async runCatalogDeploy(apiClient, yaml, flags) {
        let resolved;
        try {
            resolved = await (0, resolve_cluster_1.resolveCluster)(flags.cluster);
        }
        catch (error) {
            this.error(error.message, { exit: 1 });
        }
        const { id: clusterId, name: clusterName } = resolved;
        const envOverrides = {};
        for (const kv of flags.env ?? []) {
            const eq = kv.indexOf('=');
            if (eq < 1) {
                this.error(`Invalid --env value "${kv}". Expected KEY=VALUE format.`, {
                    exit: 1,
                });
            }
            envOverrides[kv.slice(0, eq)] = kv.slice(eq + 1);
        }
        console.log(chalk_1.default.cyan('\n  Deploy from manifest\n'));
        console.log(`  ${chalk_1.default.bold('Cluster:')} ${clusterName}`);
        if (flags.domain)
            console.log(`  ${chalk_1.default.bold('Domain:')}  ${flags.domain}`);
        if (Object.keys(envOverrides).length > 0) {
            console.log(`  ${chalk_1.default.bold('Env:')}     ${Object.keys(envOverrides).join(', ')}`);
        }
        console.log('');
        const spinner = (0, ora_1.default)('Submitting manifest…').start();
        let install;
        try {
            install = await apiClient.post('/catalog/install-from-yaml', {
                yaml,
                clusterId,
                ...(flags.name ? { displayName: flags.name } : {}),
                ...(flags.domain ? { domain: flags.domain } : {}),
                ...(flags['skip-endpoint'] ? { skipEndpoint: true } : {}),
                ...(Object.keys(envOverrides).length > 0
                    ? { envOverrides, userInputs: envOverrides }
                    : {}),
            });
            spinner.succeed('Install queued');
            console.log('');
            console.log(`  ${chalk_1.default.bold('Install ID:')}   ${install.id}`);
            console.log(`  ${chalk_1.default.bold('App:')}          ${install.displayName} (${install.slug})`);
            if (install.operationId) {
                console.log(`  ${chalk_1.default.bold('Operation:')}    ${install.operationId}`);
            }
            console.log('');
        }
        catch (error) {
            spinner.fail('Failed to submit manifest');
            const msg = error.response?.data?.message ?? error.message;
            console.log(chalk_1.default.red(`\n  Error: ${msg}\n`));
            this.exit(1);
        }
        const noWait = flags['no-wait'] || flags.detach;
        if (noWait) {
            console.log(chalk_1.default.dim(`  Use \`flui app status ${install.slug}\` to check progress.\n`));
            return;
        }
        console.log(chalk_1.default.dim(`  Waiting for install to complete (up to ${MAX_WAIT_CATALOG_MS / 60000} min)…`));
        const waitSpinner = (0, ora_1.default)(`Installing ${install.displayName}…`).start();
        const started = Date.now();
        while (Date.now() - started < MAX_WAIT_CATALOG_MS) {
            await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
            try {
                const current = await apiClient.get(`/catalog/installs/${install.id}`);
                if (this.isRunningStatus(current.status)) {
                    waitSpinner.succeed(chalk_1.default.green(`"${install.displayName}" is running`));
                    console.log('');
                    if (current.resolvedFqdn) {
                        const url = chalk_1.default.cyan(`https://${current.resolvedFqdn}`);
                        console.log(`  ${chalk_1.default.bold('URL:')} ${url}`);
                    }
                    console.log(chalk_1.default.dim(`  Run \`flui app status ${install.slug}\` for runtime details.\n`));
                    return;
                }
                if (this.isFailedStatus(current.status)) {
                    waitSpinner.fail('Install failed');
                    const msg = current.errorMessage ?? 'Unknown error';
                    console.log(chalk_1.default.red(`\n  Error: ${msg}\n`));
                    this.exit(1);
                }
                waitSpinner.text = `Installing ${install.displayName}… (${(current.status ?? '').toLowerCase()})`;
            }
            catch {
                // polling error — keep trying
            }
        }
        waitSpinner.warn('Timed out waiting for install');
        console.log(chalk_1.default.yellow(`\n  Install is still in progress. Check with:`));
        console.log(chalk_1.default.dim(`    flui app status ${install.slug}\n`));
    }
    // ── Validation ─────────────────────────────────────────────────────────────
    async validateManifest(apiClient, raw) {
        const kind = this.detectKind(raw);
        if (kind === 'Application') {
            // Local validation — send to deploy-from-yaml with validateOnly flag
            const spinner = (0, ora_1.default)('Validating manifest…').start();
            try {
                await apiClient.post('/applications/deploy-from-yaml', {
                    yaml: raw,
                    clusterId: '00000000-0000-0000-0000-000000000000',
                    repoFullName: 'validate/only',
                    validateOnly: true,
                });
                spinner.stop();
                console.log(chalk_1.default.green('\n  Manifest is valid (kind: Application)\n'));
            }
            catch (error) {
                spinner.fail('Validation failed');
                const msg = error.response?.data?.message ?? error.message;
                console.log(chalk_1.default.red(`\n  Error: ${msg}\n`));
                this.exit(1);
            }
            return;
        }
        const spinner = (0, ora_1.default)('Validating manifest…').start();
        try {
            const result = await apiClient.post('/catalog/validate', { yaml: raw });
            spinner.stop();
            if (result.valid) {
                console.log(chalk_1.default.green('\n  Manifest is valid\n'));
                if (result.checksum) {
                    console.log(chalk_1.default.dim(`  Checksum: ${result.checksum}\n`));
                }
            }
            else {
                console.log(chalk_1.default.red('\n  Manifest has errors:\n'));
                for (const err of result.errors ?? []) {
                    console.log(chalk_1.default.red(`    • ${err}`));
                }
                console.log('');
                this.exit(1);
            }
        }
        catch (error) {
            spinner.fail('Validation failed');
            const msg = error.response?.data?.message ?? error.message;
            console.log(chalk_1.default.red(`\n  Error: ${msg}\n`));
            this.exit(1);
        }
    }
    // ── Helpers ────────────────────────────────────────────────────────────────
    /**
     * Runs framework-specific pre-deploy checks against the project directory.
     * Returns `true` when the deploy should bail (warnings present, no override).
     */
    runPostChecksOrBail(projectDir, skip) {
        const detected = (0, framework_detector_1.detectFrameworkFromProject)(projectDir);
        if (!detected)
            return false;
        const issues = (0, framework_postchecks_1.runFrameworkPostChecks)(detected.framework, projectDir);
        const warnings = issues.filter((i) => i.level === 'warn');
        if (warnings.length === 0)
            return false;
        console.log('');
        for (const issue of warnings) {
            console.log(`  ${chalk_1.default.yellow('⚠')} ${chalk_1.default.bold(issue.title)}`);
            console.log(chalk_1.default.dim(`     ${issue.detail}`));
            for (const line of issue.hint.split('\n')) {
                console.log(chalk_1.default.dim(`     ${line}`));
            }
            console.log('');
        }
        if (skip) {
            console.log(chalk_1.default.dim('  --skip-checks set, proceeding despite warnings.\n'));
            return false;
        }
        console.log(chalk_1.default.red('  Refusing to deploy: framework post-checks failed. ' +
            'Fix the issues above, or re-run with --skip-checks to bypass.\n'));
        this.exit(1);
        return true;
    }
    detectKind(raw) {
        const match = /^kind:\s*(.+)$/m.exec(raw);
        return match?.[1]?.trim() ?? 'CatalogApp';
    }
    parseEnvOverrides(entries) {
        const result = {};
        for (const kv of entries) {
            const eq = kv.indexOf('=');
            if (eq < 1) {
                this.error(`Invalid --env value "${kv}". Expected KEY=VALUE format.`, {
                    exit: 1,
                });
            }
            result[kv.slice(0, eq)] = kv.slice(eq + 1);
        }
        return result;
    }
    printDeployHeader(opts) {
        console.log(chalk_1.default.cyan('\n  Deploy from source\n'));
        console.log(`  ${chalk_1.default.bold('File:')}    ${opts.filePath}`);
        console.log(`  ${chalk_1.default.bold('Cluster:')} ${opts.clusterName}`);
        console.log(`  ${chalk_1.default.bold('Repo:')}    ${opts.repoFullName}`);
        console.log(`  ${chalk_1.default.bold('Branch:')}  ${opts.branch}`);
        if (opts.explicitImage) {
            const mode = chalk_1.default.yellow(`--image ${opts.explicitImage}`);
            console.log(`  ${chalk_1.default.bold('Mode:')}    ${mode}`);
        }
        else if (opts.skipBuild) {
            console.log(`  ${chalk_1.default.bold('Mode:')}    ${chalk_1.default.yellow('--no-build (reuse latest image)')}`);
        }
        const envKeys = Object.keys(opts.envOverrides);
        if (envKeys.length > 0) {
            console.log(`  ${chalk_1.default.bold('Env:')}     ${envKeys.join(', ')}`);
        }
        console.log('');
    }
    detectGitRemote(cwd) {
        try {
            const remote = (0, node_child_process_1.execSync)('git remote get-url origin', {
                cwd,
                stdio: ['pipe', 'pipe', 'pipe'],
            })
                .toString()
                .trim();
            const match = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(remote);
            return match?.[1];
        }
        catch {
            return undefined;
        }
    }
    detectGitBranch(cwd) {
        try {
            const branch = (0, node_child_process_1.execSync)('git branch --show-current', {
                cwd,
                stdio: ['pipe', 'pipe', 'pipe'],
            })
                .toString()
                .trim();
            return branch || undefined;
        }
        catch {
            return undefined;
        }
    }
}
Deploy.description = 'Deploy an application from a flui.yaml manifest to a cluster. ' +
    'Supports both kind:CatalogApp (pre-built image) and kind:Application (source build via GitHub Actions).';
Deploy.examples = [
    '<%= config.bin %> <%= command.id %>',
    '<%= config.bin %> <%= command.id %> ./apps/api/flui.yaml',
    '<%= config.bin %> <%= command.id %> --repo acme/my-app --branch main',
    '<%= config.bin %> <%= command.id %> --detach',
    '<%= config.bin %> <%= command.id %> --env DATABASE_URL=postgres://... --env API_KEY=secret',
    '<%= config.bin %> <%= command.id %> --validate-only',
    '<%= config.bin %> <%= command.id %> --cluster my-cluster',
];
Deploy.args = {
    file: core_1.Args.string({
        description: 'Path to flui.yaml manifest (default: ./flui.yaml)',
        required: false,
    }),
};
Deploy.flags = {
    cluster: core_1.Flags.string({
        char: 'c',
        description: 'Target cluster name or ID (default: auto-detect)',
    }),
    repo: core_1.Flags.string({
        char: 'r',
        description: 'GitHub repository full name owner/repo (default: auto-detect from git remote origin). Required for kind:Application.',
    }),
    branch: core_1.Flags.string({
        char: 'b',
        description: 'Git branch to deploy (default: auto-detect current branch). Used for kind:Application.',
    }),
    domain: core_1.Flags.string({
        char: 'd',
        description: 'Custom FQDN for the app (default: auto-assign from cluster DNS zone)',
    }),
    name: core_1.Flags.string({
        description: 'Display name override (default: metadata.name from manifest)',
    }),
    env: core_1.Flags.string({
        char: 'e',
        description: 'Environment variable override in KEY=VALUE format. Repeatable.',
        multiple: true,
    }),
    detach: core_1.Flags.boolean({
        description: 'Return immediately after triggering the build/deploy without waiting for completion. ' +
            'Use `flui app status <name>` to track progress.',
        default: false,
    }),
    'no-build': core_1.Flags.boolean({
        description: 'Skip the GitHub Actions build and re-deploy the current image. ' +
            'Useful for fast iterations on flui.yaml config (env, ports, healthcheck, endpoint) without rebuilding. ' +
            'If the app was deleted, falls back to the GHCR latest tag for {owner}/{repoName}.',
        default: false,
    }),
    image: core_1.Flags.string({
        description: 'Deploy a specific image reference (e.g. ghcr.io/owner/repo:sha). ' +
            'Skips the build pipeline. Useful for rollback to a known tag, or to deploy from GHCR ' +
            'when the app was deleted from Flui.',
    }),
    'skip-endpoint': core_1.Flags.boolean({
        description: 'Skip DNS and TLS provisioning (kind:CatalogApp only)',
        default: false,
    }),
    'no-wait': core_1.Flags.boolean({
        description: 'Alias for --detach (kind:CatalogApp compat)',
        default: false,
    }),
    'validate-only': core_1.Flags.boolean({
        description: 'Validate manifest against the flui/v1 schema without deploying',
        default: false,
    }),
    'skip-checks': core_1.Flags.boolean({
        description: 'Bypass the framework post-checks (e.g. Next.js standalone output). ' +
            'Only use when you are certain the Dockerfile will succeed despite the warnings.',
        default: false,
    }),
};
exports.default = Deploy;
