"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 nest_app_1 = require("../../lib/nest-app");
const nip_base_domain_util_1 = require("../../lib/nip-base-domain.util");
const cli_observability_cluster_service_1 = require("../../services/cli-observability-cluster.service");
const cli_ssh_service_1 = require("../../services/cli-ssh.service");
const cloud_provider_enum_1 = require("../../../../src/modules/providers/enums/cloud-provider.enum");
const cluster_entity_1 = require("../../../../src/modules/infrastructure/clusters/entities/cluster.entity");
const firewall_provider_factory_1 = require("../../../../src/modules/providers/core/factories/firewall-provider.factory");
const ip_detection_1 = require("../../lib/utils/ip-detection");
const cli_firewall_repository_1 = require("../../lib/repositories/cli-firewall.repository");
const firewall_rules_1 = require("../../lib/templates/firewall-rules");
const vnet_provisioning_service_1 = require("../../lib/services/vnet-provisioning.service");
const api_client_1 = require("../../lib/api-client");
const config_storage_1 = require("../../lib/config-storage");
const provider_credential_schemas_1 = require("../../lib/provider-credential-schemas");
const context_banner_1 = require("../../lib/context-banner");
const server_type_cache_service_1 = require("../../services/server-type-cache.service");
const server_type_validator_service_1 = require("../../services/server-type-validator.service");
const defaults_1 = require("../../config/defaults");
const prompts_1 = require("../../lib/prompts");
const preferences_resolver_1 = require("../../config/preferences-resolver");
const preferences_schema_1 = require("../../config/preferences-schema");
class EnvCreate extends core_1.Command {
    async run() {
        const { flags } = await this.parse(EnvCreate);
        const providerKey = flags.provider;
        const cloudProvider = providerKey === 'scaleway'
            ? cloud_provider_enum_1.CloudProvider.SCALEWAY
            : cloud_provider_enum_1.CloudProvider.HETZNER;
        const nodeSize = flags['node-size'] || (0, defaults_1.getRecommendedServerType)(providerKey);
        const region = flags.region || (0, defaults_1.getDefaultRegion)(providerKey);
        const allowedRegions = (0, defaults_1.getEuRegions)(providerKey);
        if (!allowedRegions.includes(region)) {
            this.error(`Region '${region}' is not supported for provider '${providerKey}'. Allowed: ${allowedRegions.join(', ')}`, { exit: 1 });
        }
        if (flags['auth-mode'] !== 'oidc') {
            this.error(`Authentication mode '${flags['auth-mode']}' is not supported. Only 'oidc' is available.`, { exit: 1 });
        }
        (0, context_banner_1.printContextBanner)({
            cluster: { provider: providerKey, region },
        });
        let spinner = (0, ora_1.default)('Initializing...').start();
        let app;
        let firewallId;
        let sourceCidrs;
        try {
            // 1. Bootstrap NestJS and get services
            app = await (0, nest_app_1.getNestApp)();
            spinner.succeed('Initialized');
            spinner = (0, ora_1.default)('Loading services...').start();
            const configStorage = new config_storage_1.ConfigStorage();
            const apiUrl = configStorage.getApiUrl();
            const observabilityService = app.get(cli_observability_cluster_service_1.CliObservabilityClusterService);
            const apiClient = new api_client_1.ApiClient({
                baseUrl: apiUrl,
                apiKey: configStorage.getApiKey(),
            });
            const cacheService = new server_type_cache_service_1.ServerTypeCacheService();
            const validatorService = new server_type_validator_service_1.ServerTypeValidatorService();
            spinner.succeed('Services loaded');
            // 2. Early credential check — must happen before any provider call
            const hasProviderCreds = (0, provider_credential_schemas_1.isCompoundProvider)(providerKey)
                ? configStorage.hasCredentials(providerKey)
                : configStorage.hasToken(providerKey);
            if (!hasProviderCreds) {
                spinner.stop();
                const configured = await (0, prompts_1.runProviderSetupWizard)();
                if (!configured) {
                    console.log(chalk_1.default.dim(`   To configure manually: flui config set ${providerKey}\n`));
                    this.exit(1);
                }
                spinner = (0, ora_1.default)('Resuming...').start();
                spinner.succeed('Provider configured');
            }
            // 3. Resolve admin email (prompt if not set, then persist)
            const emailResolver = new preferences_resolver_1.PreferencesResolver(configStorage);
            const emailResult = emailResolver.resolve('email');
            let adminEmail;
            if (emailResult.value) {
                adminEmail = emailResult.value;
            }
            else {
                spinner.stop();
                adminEmail = await (0, prompts_1.promptInput)({
                    message: "Admin email (used for Let's Encrypt and notifications)",
                    validate: preferences_schema_1.PREFERENCES.email.validate,
                });
                configStorage.setPreference('email', adminEmail);
                spinner = (0, ora_1.default)('Resuming...').start();
                spinner.succeed('Email saved');
            }
            // ACME server choice: production by default. Staging only via explicit
            // --acme-staging flag (untrusted certs break SSO discovery, so we never
            // auto-fall-back). Each cluster gets a unique nip.io hostname token
            // server-side, so the LE 5-certs-per-7-days rate limit never trips on
            // repeated test creations.
            const acmeStaging = flags['acme-staging'];
            spinner.stop();
            if (acmeStaging) {
                console.log(chalk_1.default.yellow(`\n⚠ ACME endpoint: Let's Encrypt STAGING — cert will not be browser-trusted (warning expected).\n`));
            }
            else {
                const recent = configStorage
                    .getAcmeIssuances()
                    .filter((i) => i.server === 'production');
                const byDomain = recent.reduce((acc, i) => {
                    acc[i.domains] = (acc[i.domains] ?? 0) + 1;
                    return acc;
                }, {});
                if (recent.length > 0) {
                    const lines = Object.entries(byDomain)
                        .map(([d, n]) => `   • ${d} (${n})`)
                        .join('\n');
                    console.log(chalk_1.default.dim(`\nℹ ACME endpoint: Let's Encrypt PRODUCTION (browser-trusted cert).\n` +
                        `   Issuances seen in the last 7 days (per domain set):\n${lines}\n`));
                }
                else {
                    console.log(chalk_1.default.dim(`\nℹ ACME endpoint: Let's Encrypt PRODUCTION (browser-trusted cert).\n`));
                }
            }
            spinner = (0, ora_1.default)('Resuming...').start();
            // 3. Validate server type with multi-region fallback
            spinner = (0, ora_1.default)('Validating server type and region availability...').start();
            let validatedNodeSize = nodeSize;
            let validatedRegion = region;
            let serverTypes = await cacheService.get(providerKey);
            if (!serverTypes) {
                spinner.text = 'Fetching available server types...';
                try {
                    serverTypes = await apiClient.getNodeSizes(providerKey);
                    await cacheService.set(providerKey, serverTypes);
                }
                catch (error) {
                    spinner.warn(`Could not fetch server types: ${error.message}. Proceeding with requested type.`);
                    serverTypes = [];
                }
            }
            if (serverTypes.length > 0) {
                const euRegions = allowedRegions;
                const fallbackRegions = euRegions.filter((r) => r !== region);
                const fallbackTypes = (0, defaults_1.getFallbackServerTypes)(providerKey);
                const validation = validatorService.validateServerTypeWithRegionFallback(nodeSize, serverTypes, region, fallbackRegions, fallbackTypes);
                if (!validation.isValid) {
                    spinner.fail(validation.reason);
                    if (validation.suggestedAlternative) {
                        const regionInfo = validation.suggestedRegion &&
                            validation.suggestedRegion !== region
                            ? ` in region ${validation.suggestedRegion}`
                            : '';
                        console.log(chalk_1.default.yellow(`\n💡 Suggested alternative: ${validation.suggestedAlternative.name}${regionInfo}\n`));
                        const oldPrice = nodeSize === 'cx22' ? '7.50' : undefined;
                        const confirmed = await (0, prompts_1.confirmAlternativeServerType)(nodeSize, validation.suggestedAlternative, oldPrice);
                        if (confirmed) {
                            validatedNodeSize = validation.suggestedAlternative.id;
                            if (validation.suggestedRegion &&
                                validation.suggestedRegion !== region) {
                                validatedRegion = validation.suggestedRegion;
                                console.log(chalk_1.default.green(`✓ Using ${validation.suggestedAlternative.name} in region ${validatedRegion}\n`));
                            }
                            else {
                                console.log(chalk_1.default.green(`✓ Using ${validation.suggestedAlternative.name}\n`));
                            }
                        }
                        else {
                            // Let user select manually from available types in any EU region
                            const availableTypes = serverTypes.filter((t) => {
                                if (t.deprecated)
                                    return false;
                                // Use real-time availability if present
                                if (t.availability && t.availability.length > 0) {
                                    return t.availability.some((av) => euRegions.includes(av.location) &&
                                        av.available &&
                                        !av.deprecated);
                                }
                                // Fallback to locations
                                return t.locations.some((loc) => euRegions.includes(loc.name) && !loc.deprecation);
                            });
                            const selected = await (0, prompts_1.selectServerTypePrompt)(availableTypes, 'Please select a server type:');
                            if (selected) {
                                validatedNodeSize = selected.id;
                                // Find best region for selected type
                                let selectedRegions;
                                if (selected.availability && selected.availability.length > 0) {
                                    selectedRegions = selected.availability
                                        .filter((av) => euRegions.includes(av.location) &&
                                        av.available &&
                                        !av.deprecated)
                                        .map((av) => av.location);
                                }
                                else {
                                    selectedRegions = selected.locations
                                        .filter((loc) => euRegions.includes(loc.name) && !loc.deprecation)
                                        .map((loc) => loc.name);
                                }
                                if (!selectedRegions.includes(region) &&
                                    selectedRegions.length > 0) {
                                    validatedRegion = selectedRegions[0];
                                    console.log(chalk_1.default.yellow(`Note: ${selected.name} is not available in ${region}, using ${validatedRegion} instead`));
                                }
                                console.log(chalk_1.default.green(`✓ Using ${selected.name}\n`));
                            }
                            else {
                                console.log(chalk_1.default.red('Operation cancelled by user\n'));
                                this.exit(0);
                            }
                        }
                    }
                    else {
                        (0, prompts_1.displayServerTypeNotFoundError)(nodeSize, providerKey);
                        console.log(chalk_1.default.yellow(`Run: flui server-types list --provider ${providerKey}\n`));
                        this.exit(1);
                    }
                }
                else if (validation.suggestedRegion &&
                    validation.suggestedRegion !== region) {
                    validatedRegion = validation.suggestedRegion;
                    spinner.succeed(`Server type validated (available in ${validatedRegion} instead of ${region})`);
                }
                else {
                    spinner.succeed('Server type and region validated');
                }
            }
            else {
                spinner.succeed('Server type validation skipped (no data available)');
            }
            // Display cluster configuration
            console.log(chalk_1.default.cyan('\n🚀 Creating Flui Observability Cluster (K3s)\n'));
            console.log(chalk_1.default.dim(`   Provider: ${providerKey}`));
            console.log(chalk_1.default.dim(`   Node Size: ${validatedNodeSize}`));
            console.log(chalk_1.default.dim(`   Region: ${validatedRegion}`));
            console.log(chalk_1.default.dim(`   Worker Nodes: ${flags['node-count']}\n`));
            // 3. Check if observability cluster already exists
            spinner = (0, ora_1.default)('Checking for existing cluster...').start();
            const existingCluster = await observabilityService.getObservabilityCluster();
            if (existingCluster && existingCluster.status !== cluster_entity_1.ClusterStatus.DELETED) {
                spinner.fail('Observability cluster already exists!');
                console.log(chalk_1.default.yellow('\n⚠️  An observability cluster is already running:\n'));
                console.log(`   ${chalk_1.default.bold('Name:')}     ${existingCluster.name}`);
                console.log(`   ${chalk_1.default.bold('ID:')}       ${existingCluster.id}`);
                console.log(`   ${chalk_1.default.bold('Status:')}   ${existingCluster.status}`);
                console.log(`   ${chalk_1.default.bold('Nodes:')}    ${existingCluster.nodeCount}`);
                console.log(`   ${chalk_1.default.bold('Region:')}   ${existingCluster.region}`);
                console.log(chalk_1.default.yellow('\nUse the following commands to manage it:'));
                console.log(`   ${chalk_1.default.cyan('flui env status')}   - Check status`);
                console.log(`   ${chalk_1.default.cyan('flui env destroy')}  - Delete permanently\n`);
                return;
            }
            spinner.succeed('No existing cluster found');
            // 3.5. Provision environment VNet+Subnet BEFORE the cluster.
            //      Every server (master + workers) and every future workload cluster
            //      joins this private network so intra/inter-cluster traffic stays
            //      off the public interface.
            let envVnetInfo;
            try {
                spinner = (0, ora_1.default)('Provisioning environment VNet/Subnet...').start();
                const vnetService = app.get(vnet_provisioning_service_1.VnetProvisioningService);
                envVnetInfo = await vnetService.ensureEnvVnet({
                    provider: cloudProvider,
                    name: 'flui-env-vnet',
                    ipRange: '10.10.0.0/16',
                    subnetIpRange: '10.10.1.0/24',
                    networkZone: cloudProvider === cloud_provider_enum_1.CloudProvider.HETZNER
                        ? vnetService.hetznerNetworkZoneFor(validatedRegion)
                        : undefined,
                    region: cloudProvider === cloud_provider_enum_1.CloudProvider.SCALEWAY
                        ? validatedRegion
                        : undefined,
                });
                spinner.succeed(`VNet ready: ${envVnetInfo.vnetProviderResourceId} (subnet ${envVnetInfo.subnetIpRange})`);
            }
            catch (error) {
                spinner.fail(`VNet provisioning failed: ${error.message}`);
                console.log(chalk_1.default.red('\n   Cannot proceed without an environment VNet. The observability cluster and all workload clusters must share a private network.'));
                this.exit(1);
            }
            // 4. Create firewall BEFORE cluster (if enabled)
            if (flags['configure-firewall']) {
                spinner = (0, ora_1.default)('Creating firewall...').start();
                try {
                    const ipService = app.get(ip_detection_1.IpDetectionService);
                    const firewallFactory = app.get(firewall_provider_factory_1.FirewallProviderFactory);
                    const firewallService = firewallFactory.getFirewallProviderOrFail(cloudProvider);
                    const firewallRepo = app.get(cli_firewall_repository_1.CliFirewallRepository);
                    // Detect or parse source IP/CIDR
                    if (flags['firewall-ip']) {
                        sourceCidrs = ipService.parseCidrList(flags['firewall-ip']);
                    }
                    else {
                        const publicIp = await ipService.getPublicIp();
                        sourceCidrs = [ipService.toCidr(publicIp)];
                    }
                    const rules = (0, firewall_rules_1.OBSERVABILITY_FIREWALL_RULES)(sourceCidrs);
                    const temporaryFirewallName = `flui-observability-firewall-${Date.now()}`;
                    // Create firewall with temporary name (will be renamed when cluster is ready)
                    const result = await firewallService.createFirewall({
                        name: temporaryFirewallName,
                        labels: [
                            { key: 'managed-by', value: 'flui-cloud' },
                            { key: 'flui-resource-type', value: 'firewall' },
                            { key: 'flui-cluster-type', value: 'observability' },
                        ],
                        rules,
                        // Label selector will be applied later when cluster servers exist
                    });
                    firewallId = result.firewallId;
                    await firewallRepo.save({
                        id: firewallId,
                        name: temporaryFirewallName,
                        provider: cloudProvider.toUpperCase(),
                        clusterId: '',
                        rules,
                        sourceCidrs,
                        appliedToServerIds: [],
                        labels: [
                            { key: 'managed-by', value: 'flui-cloud' },
                            { key: 'flui-resource-type', value: 'firewall' },
                        ],
                    });
                    spinner.succeed(`Firewall created (Source: ${sourceCidrs.join(', ')})`);
                }
                catch (error) {
                    spinner.warn(`Firewall creation failed: ${error.message}`);
                    console.log(chalk_1.default.yellow('   Continuing without firewall. You can configure it manually later.'));
                    firewallId = undefined;
                    sourceCidrs = undefined;
                }
            }
            // 5. Create K3s cluster
            spinner = (0, ora_1.default)('Creating K3s cluster...').start();
            console.log(chalk_1.default.dim(flags['node-count'] > 0
                ? '   This will create master node + worker nodes'
                : '   This will create master node only'));
            const clusterId = await observabilityService.createObservabilityCluster(cloudProvider, validatedRegion, validatedNodeSize, flags['node-count'], firewallId, sourceCidrs, flags['auth-mode'], envVnetInfo
                ? {
                    vnetProviderResourceId: envVnetInfo.vnetProviderResourceId,
                    vnetIpRange: envVnetInfo.vnetIpRange,
                    subnetProviderResourceId: envVnetInfo.subnetProviderResourceId,
                    subnetIpRange: envVnetInfo.subnetIpRange,
                    subnetType: envVnetInfo.subnetType,
                    networkZone: envVnetInfo.networkZone,
                }
                : undefined, adminEmail, acmeStaging, flags['disk-size'], {
                sharedStorageEnabled: !flags['no-shared-storage'],
                sharedStorageVolumeSizeGb: flags['shared-storage-size'],
            });
            spinner.succeed('Cluster creation started!');
            if (firewallId && clusterId) {
                try {
                    const fwRepo = app.get(cli_firewall_repository_1.CliFirewallRepository);
                    const existing = await fwRepo.findById(firewallId);
                    if (existing && !existing.clusterId) {
                        await fwRepo.save({ ...existing, clusterId });
                    }
                }
                catch (e) {
                    console.log(chalk_1.default.yellow(`   ⚠️  Could not link firewall ${firewallId} to cluster ${clusterId}: ${e.message}`));
                }
            }
            // Validate mutual exclusivity
            if (flags.wait && flags.detached) {
                this.error('Cannot use --wait and --detached together');
            }
            if (flags.detached) {
                // DETACHED MODE: Exit immediately with monitoring instructions
                console.log(chalk_1.default.green('\n✅ Observability Cluster Creation Started!\n'));
                console.log(chalk_1.default.cyan('📋 Cluster Details:\n'));
                console.log(`   ${chalk_1.default.bold('Cluster ID:')}  ${clusterId}`);
                console.log(`   ${chalk_1.default.bold('Provider:')}    ${cloudProvider}`);
                console.log(`   ${chalk_1.default.bold('Region:')}      ${validatedRegion}`);
                console.log(`   ${chalk_1.default.bold('Node Size:')}   ${validatedNodeSize}`);
                console.log(`   ${chalk_1.default.bold('Worker Nodes:')} ${flags['node-count']}`);
                console.log(`   ${chalk_1.default.bold('Status:')}      ${chalk_1.default.yellow('Creating in background')}`);
                console.log(chalk_1.default.cyan('\n📊 Monitor Progress:\n'));
                console.log(`   ${chalk_1.default.cyan('flui env status')}   - Check cluster creation progress`);
                if (firewallId && sourceCidrs) {
                    console.log(chalk_1.default.cyan('\n🔥 Firewall:\n'));
                    console.log(chalk_1.default.green(`   ✅ Firewall created (Source: ${sourceCidrs.join(', ')})`));
                    console.log(chalk_1.default.dim('   Will be applied automatically when cluster is ready.'));
                }
                console.log(chalk_1.default.cyan('\n💡 Tip: Run without -d to follow provisioning logs in real-time\n'));
                console.log('');
            }
            else if (flags.wait) {
                // WAIT MODE: Wait for cluster to be ready
                spinner = (0, ora_1.default)('Waiting for cluster to be ready...').start();
                try {
                    await observabilityService.waitForClusterReady(clusterId, 600000);
                    spinner.succeed('Cluster is ready!');
                }
                catch (error) {
                    spinner.fail('Failed to wait for cluster');
                    console.log(chalk_1.default.red(`\n❌ Error: ${error.message}\n`));
                    this.exit(1);
                }
                console.log(chalk_1.default.green('\n✅ Observability Cluster Created Successfully!\n'));
                console.log(chalk_1.default.cyan('📋 Cluster Details:\n'));
                console.log(`   ${chalk_1.default.bold('Cluster ID:')}  ${clusterId}`);
                console.log(`   ${chalk_1.default.bold('Provider:')}    ${cloudProvider}`);
                console.log(`   ${chalk_1.default.bold('Region:')}      ${validatedRegion}`);
                console.log(`   ${chalk_1.default.bold('Node Size:')}   ${validatedNodeSize}`);
                console.log(`   ${chalk_1.default.bold('Worker Nodes:')} ${flags['node-count']}`);
                if (firewallId && sourceCidrs) {
                    console.log(chalk_1.default.cyan('\n🔥 Firewall:\n'));
                    console.log(chalk_1.default.green(`   ✅ Firewall configured and applied (Source: ${sourceCidrs.join(', ')})`));
                }
                console.log(chalk_1.default.cyan('\n📊 Manage Cluster:\n'));
                console.log(`   ${chalk_1.default.cyan('flui env status')}   - Check cluster status`);
                console.log(`   ${chalk_1.default.cyan('flui env destroy')}  - Delete cluster`);
                console.log(chalk_1.default.cyan('\n🔑 SSH Access:\n'));
                console.log(`   ${chalk_1.default.cyan('flui ssh master')}   - SSH to master node`);
                if (flags['node-count'] > 0) {
                    console.log(`   ${chalk_1.default.cyan('flui ssh worker-1')} - SSH to worker node`);
                }
                console.log('');
            }
            else {
                // DEFAULT MODE: Follow logs + auto-reconcile on READY
                const sshService = app.get(cli_ssh_service_1.CliSshService);
                let pollerHandle = null;
                // Start background poller immediately (independent of SSH)
                // This ensures reconciliation runs even if SSH setup fails
                let shouldExit = false;
                const onReconcile = async () => {
                    const cluster = await observabilityService.getObservabilityCluster();
                    const masterIp = cluster?.masterIpAddress;
                    const nipToken = cluster?.nipHostnameToken;
                    const baseDomain = (0, nip_base_domain_util_1.buildNipBaseDomain)(masterIp ?? '', nipToken);
                    if (masterIp) {
                        const certUrl = `https://auth.${baseDomain}`;
                        console.log(chalk_1.default.dim(`\n→ Waiting for website certificate (Let's Encrypt)...`));
                        let elapsed = 0;
                        const maxWait = 300_000;
                        const interval = 15_000;
                        const deadline = Date.now() + maxWait;
                        while (Date.now() < deadline) {
                            const valid = await observabilityService.waitForValidTls(certUrl, interval, interval, acmeStaging);
                            if (valid)
                                break;
                            elapsed += interval;
                            console.log(chalk_1.default.dim(`  Certificate pending... (${Math.round(elapsed / 1000)}s / ${maxWait / 1000}s)`));
                        }
                        const tlsReady = await observabilityService.waitForValidTls(certUrl, 5_000, 5_000, acmeStaging);
                        if (tlsReady) {
                            console.log(chalk_1.default.green(acmeStaging
                                ? "✅ TLS certificate valid (Let's Encrypt STAGING — not browser-trusted)"
                                : '✅ TLS certificate valid'));
                            configStorage.recordAcmeIssuance({
                                domains: `auth/api/app.${baseDomain}`,
                                server: acmeStaging ? 'staging' : 'production',
                            });
                        }
                        else {
                            console.log(chalk_1.default.yellow('⚠ Certificate not ready — cluster usable but browser may show SSL warning'));
                        }
                        const apiBaseUrl = `https://api.${baseDomain}`;
                        if (flags['auth-mode'] === 'oidc') {
                            const fluiApiKey = cluster?.metadata?.fluiApiKey;
                            if (fluiApiKey) {
                                console.log(chalk_1.default.dim(`\n→ Triggering OIDC bootstrap (Zitadel project, apps, admin)...`));
                                try {
                                    const apiClient = new api_client_1.ApiClient({
                                        baseUrl: `${apiBaseUrl}/api/v1`,
                                        apiKey: fluiApiKey,
                                    });
                                    await apiClient.post('/auth/bootstrap');
                                    console.log(chalk_1.default.green('✅ OIDC bootstrap triggered'));
                                }
                                catch (err) {
                                    console.log(chalk_1.default.yellow(`⚠ OIDC bootstrap call failed (${err.message}) — will retry via polling`));
                                }
                            }
                            console.log(chalk_1.default.dim(`\n→ Waiting for OIDC client provisioning...`));
                            const oidcReady = await observabilityService.waitForOidcReady(apiBaseUrl, 300_000, 10_000, acmeStaging);
                            if (oidcReady) {
                                console.log(chalk_1.default.green('✅ OIDC login ready'));
                            }
                            else {
                                console.log(chalk_1.default.yellow('⚠ OIDC not ready — run `flui env force-ready` to retry bootstrap'));
                            }
                        }
                    }
                    console.log(chalk_1.default.green('\n' +
                        '─'.repeat(80) +
                        '\n✅ Cluster is READY!\n' +
                        '─'.repeat(80) +
                        '\n'));
                    if (masterIp) {
                        console.log(`   ${chalk_1.default.bold('Frontend:')} https://app.${baseDomain}`);
                    }
                    console.log(chalk_1.default.bold('\n   To retrieve your credentials, run:'));
                    console.log(chalk_1.default.cyan('   flui env credentials\n'));
                    // Signal auto-exit
                    shouldExit = true;
                };
                const onFailed = (errorMsg) => {
                    console.log(chalk_1.default.red('\n\n' +
                        '─'.repeat(80) +
                        `\n❌ Cluster creation failed: ${errorMsg}\n` +
                        '─'.repeat(80) +
                        '\n'));
                    shouldExit = true;
                };
                pollerHandle = observabilityService.pollOperationUntilReady(clusterId, onReconcile, onFailed);
                // Phase 1: Wait for server provisioning (master IP)
                spinner = (0, ora_1.default)('Provisioning server...').start();
                let masterIp;
                try {
                    masterIp = await observabilityService.waitForMasterIp(clusterId, 600000, 5000);
                    spinner.succeed(`Server provisioned (${masterIp})`);
                }
                catch (error) {
                    spinner.fail('Server provisioning failed');
                    console.log(chalk_1.default.red(`\n❌ Error: ${error.message}\n`));
                    console.log(chalk_1.default.yellow('You can check status manually:'));
                    console.log(`   ${chalk_1.default.cyan('flui env status')}\n`);
                    console.log(chalk_1.default.dim('Waiting for cluster to finish (reconciliation will run automatically)...\n'));
                    await pollerHandle.done;
                    await (0, nest_app_1.closeNestApp)();
                    process.exit(0);
                }
                // Phase 2: Wait for server boot (SSH port open)
                spinner = (0, ora_1.default)('Server booting...').start();
                try {
                    await observabilityService.waitForPortReady(masterIp, 22, 600000, 5000);
                    spinner.succeed('Server online');
                }
                catch (error) {
                    spinner.fail('Server did not come online in time');
                    console.log(chalk_1.default.red(`\n❌ Error: ${error.message}\n`));
                    console.log(chalk_1.default.dim('Waiting for cluster to finish (reconciliation will run automatically)...\n'));
                    await pollerHandle.done;
                    await (0, nest_app_1.closeNestApp)();
                    process.exit(0);
                }
                // Phase 3: Wait for SSH authentication (CA enrollment via cloud-init)
                spinner = (0, ora_1.default)('Waiting for SSH access (CA enrollment)...').start();
                try {
                    await observabilityService.waitForSshAuth(() => sshService.sshExec(masterIp, 'echo ok'), 600000, 10000);
                    spinner.succeed('SSH access ready');
                }
                catch (error) {
                    spinner.fail('SSH access did not become ready in time');
                    console.log(chalk_1.default.red(`\n❌ Error: ${error.message}\n`));
                    console.log(chalk_1.default.yellow('You can inspect logs manually when ready:'));
                    console.log(`   ${chalk_1.default.cyan('flui env inspect -f')}\n`);
                    console.log(chalk_1.default.dim('Waiting for cluster to finish (reconciliation will run automatically)...\n'));
                    await pollerHandle.done;
                    await (0, nest_app_1.closeNestApp)();
                    process.exit(0);
                }
                // Phase 4: Stream cloud-init-output logs
                const logPath = '/var/log/cloud-init-output.log';
                console.log(chalk_1.default.cyan('\n📋 Streaming cloud-init-output from master node\n'));
                console.log(chalk_1.default.dim(`   Node: master (${masterIp})`));
                console.log(chalk_1.default.dim(`   Log: ${logPath}`));
                console.log(chalk_1.default.dim(`   Press Ctrl+C to stop\n`));
                console.log(chalk_1.default.dim('─'.repeat(80)));
                let sshCleanup = null;
                const exitHandler = () => {
                    console.log(chalk_1.default.dim('\n\n' + '─'.repeat(80)));
                    console.log(chalk_1.default.yellow('\nStopping log stream...\n'));
                    if (pollerHandle) {
                        pollerHandle.stop();
                    }
                    if (sshCleanup) {
                        sshCleanup();
                    }
                    (0, nest_app_1.closeNestApp)().then(() => process.exit(0));
                };
                process.on('SIGINT', exitHandler);
                process.on('SIGTERM', exitHandler);
                try {
                    const stream = await sshService.streamRemoteLog(masterIp, logPath);
                    sshCleanup = stream.cleanup;
                    // Keep streaming until reconciliation completes or Ctrl+C
                    await new Promise((resolve) => {
                        const checkExit = setInterval(() => {
                            if (shouldExit) {
                                clearInterval(checkExit);
                                resolve();
                            }
                        }, 1000);
                    });
                    // Auto-exit after reconciliation
                    if (sshCleanup) {
                        sshCleanup();
                    }
                    await (0, nest_app_1.closeNestApp)();
                    process.exit(0);
                }
                catch (error) {
                    console.log(chalk_1.default.red('\n❌ SSH Stream Error:\n'));
                    console.log(`   ${error.message}\n`);
                    console.log(chalk_1.default.yellow('Troubleshooting:'));
                    console.log('   - The node may still be booting');
                    console.log('   - Check firewall rules allow SSH');
                    console.log(chalk_1.default.dim('\nTry again with:'));
                    console.log(`   ${chalk_1.default.cyan('flui env inspect -f')}\n`);
                    console.log(chalk_1.default.dim('Waiting for cluster to finish (reconciliation will run automatically)...\n'));
                    await pollerHandle.done;
                    await (0, nest_app_1.closeNestApp)();
                    process.exit(0);
                }
            }
        }
        catch (error) {
            spinner.fail('Failed to create observability cluster');
            if (firewallId) {
                try {
                    spinner = (0, ora_1.default)('Cleaning up orphaned firewall...').start();
                    const firewallFactory = app.get(firewall_provider_factory_1.FirewallProviderFactory);
                    const firewallService = firewallFactory.getFirewallProviderOrFail(cloudProvider);
                    const firewallRepo = app.get(cli_firewall_repository_1.CliFirewallRepository);
                    await firewallService.deleteFirewall(firewallId);
                    await firewallRepo.delete(firewallId);
                    spinner.succeed('Orphaned firewall cleaned up');
                }
                catch (cleanupError) {
                    spinner.warn(`Failed to cleanup firewall ${firewallId}: ${cleanupError.message}`);
                    console.log(chalk_1.default.yellow(`   You may need to manually delete firewall ${firewallId}`));
                }
            }
            console.log(chalk_1.default.red('\n❌ Error:\n'));
            if (error instanceof Error) {
                console.log(`   ${error.message}`);
                if (error.message.includes('already exists')) {
                    console.log(chalk_1.default.yellow('\n💡 Hint:'));
                    console.log(`   Delete the existing cluster first:`);
                    console.log(`   ${chalk_1.default.cyan('flui env destroy')}\n`);
                }
                else if (error.message.includes('token') ||
                    error.message.includes('credentials')) {
                    console.log(chalk_1.default.yellow('\n💡 Hint:'));
                    console.log(`   Make sure you've set your ${providerKey} API token:`);
                    const setCmd = chalk_1.default.cyan(`flui config set ${providerKey} YOUR_TOKEN`);
                    console.log(`   ${setCmd}\n`);
                }
            }
            else {
                console.log(`   ${String(error)}`);
            }
            this.exit(1);
        }
        finally {
            await (0, nest_app_1.closeNestApp)();
        }
    }
}
EnvCreate.description = 'Create observability cluster infrastructure on K3s';
EnvCreate.examples = [
    '<%= config.bin %> <%= command.id %>',
    '<%= config.bin %> <%= command.id %> --node-size cx32',
    '<%= config.bin %> <%= command.id %> -d',
    '<%= config.bin %> <%= command.id %> --region fsn1',
    '<%= config.bin %> <%= command.id %> --wait',
];
EnvCreate.flags = {
    provider: core_1.Flags.string({
        char: 'p',
        description: 'Cloud provider for the observability cluster',
        default: 'hetzner',
        options: ['hetzner', 'scaleway'],
    }),
    'node-size': core_1.Flags.string({
        description: 'Server type for cluster nodes (run "flui server-types list --provider <provider>" to see available types). Defaults to the provider recommended size.',
    }),
    region: core_1.Flags.string({
        char: 'r',
        description: 'Region/location code. Defaults to the provider default region.',
    }),
    'node-count': core_1.Flags.integer({
        description: 'Number of worker nodes (0 = master-only, default: 0)',
        default: 0,
    }),
    'disk-size': core_1.Flags.integer({
        description: 'Root disk size in GB. ' +
            'Required for network-storage instance types (Scaleway PRO2, ENT1). ' +
            'Optional for local-SSD types — defaults to the included size of the instance type.',
    }),
    detached: core_1.Flags.boolean({
        char: 'd',
        description: 'Detached mode: start cluster creation and exit immediately',
        default: false,
    }),
    wait: core_1.Flags.boolean({
        description: 'Wait for cluster to be ready before returning (polls operation status)',
        default: false,
    }),
    'configure-firewall': core_1.Flags.boolean({
        description: 'Automatically configure firewall after cluster creation',
        default: true,
    }),
    'firewall-ip': core_1.Flags.string({
        description: 'Source IP/CIDR for firewall (comma-separated, default: auto-detect)',
    }),
    'auth-mode': core_1.Flags.string({
        description: 'Authentication mode (only oidc is supported)',
        default: 'oidc',
    }),
    'acme-staging': core_1.Flags.boolean({
        description: "Use Let's Encrypt staging endpoint (untrusted cert, no rate limits) — useful while iterating to avoid burning prod quota",
        default: false,
    }),
    'no-shared-storage': core_1.Flags.boolean({
        description: 'Disable Flui shared storage (NFS+fscache). Default: shared storage enabled — master gets a Volume hosting the NFS export, workers mount it. Disable to fall back to local-path on each node bundled disk.',
        default: false,
    }),
    'shared-storage-size': core_1.Flags.integer({
        description: 'Size in GB of the master shared storage Volume (only when shared storage is enabled). Default: 20.',
        default: 20,
        min: 10,
    }),
};
exports.default = EnvCreate;
