"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
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 fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const child_process_1 = require("child_process");
const chalk_1 = __importDefault(require("chalk"));
const ora_1 = __importDefault(require("ora"));
const cli_k3s_script_service_1 = require("../../services/cli-k3s-script.service");
const cli_logger_service_1 = require("../../services/cli-logger.service");
const byos_precheck_1 = require("../../lib/byos-precheck");
const DEFAULT_K3S_VERSION = 'v1.35.4+k3s1';
class StandaloneInstall extends core_1.Command {
    async run() {
        const { flags } = await this.parse(StandaloneInstall);
        const sshKey = flags['ssh-key'];
        if (!fs.existsSync(sshKey)) {
            this.error(`SSH key not found at ${sshKey}`, { exit: 1 });
        }
        const target = {
            host: flags.host,
            user: flags.user,
            privateKeyPath: sshKey,
        };
        this.log(chalk_1.default.bold(`\nFlui standalone install → ${target.user}@${target.host}`));
        this.log(chalk_1.default.dim(`  ssh-key: ${sshKey}`));
        // ── Precheck ──────────────────────────────────────────────────────
        if (!flags['skip-precheck']) {
            const spinner = (0, ora_1.default)('Running system compatibility check...').start();
            let report;
            try {
                report = await (0, byos_precheck_1.runPrecheck)(target);
                spinner.succeed('System compatibility check complete');
            }
            catch (err) {
                spinner.fail(`Precheck failed: ${err.message}`);
                this.exit(1);
            }
            this.printReport(report);
            const decided = await this.gateOnReport(report, flags['best-effort']);
            if (!decided)
                this.exit(1);
        }
        else {
            this.warn('precheck skipped via --skip-precheck');
        }
        // ── Build the same bootstrap script the cloud flow generates ──────
        const clusterId = flags['cluster-id'] || crypto.randomUUID();
        const serverId = crypto.randomBytes(16).toString('hex');
        const instanceId = `${clusterId}-master`;
        const instanceName = `${flags['cluster-name']}-master`;
        const masterIp = flags['master-public-ip'] || flags.host;
        const dashedIp = masterIp.replace(/\./g, '-');
        const nipToken = `flui-${crypto.randomBytes(3).toString('hex')}`;
        const baseDomain = `${nipToken}.${dashedIp}.nip.io`;
        const adminPassword = flags['admin-password'] || randomPwd(24);
        const k3sToken = crypto.randomBytes(32).toString('hex');
        const jwtSecret = crypto.randomBytes(32).toString('hex');
        const postgresPassword = randomPwd(24);
        const redisPassword = randomPwd(24);
        const grafanaPassword = randomPwd(24);
        // Persist credentials before doing anything destructive remotely.
        const credentialsDir = path.resolve(process.cwd(), `.flui-byos-${flags['cluster-name']}-${Date.now()}`);
        fs.mkdirSync(credentialsDir, { recursive: true, mode: 0o700 });
        const credPath = path.join(credentialsDir, 'credentials.env');
        fs.writeFileSync(credPath, [
            `# Generated ${new Date().toISOString()} — keep this file safe.`,
            `CLUSTER_ID=${clusterId}`,
            `SERVER_ID=${serverId}`,
            `INSTANCE_ID=${instanceId}`,
            `INSTANCE_NAME=${instanceName}`,
            `MASTER_PUBLIC_IP=${masterIp}`,
            `FLUI_BASE_DOMAIN=${baseDomain}`,
            `NIP_HOSTNAME_TOKEN=${nipToken}`,
            `K3S_TOKEN=${k3sToken}`,
            `JWT_SECRET=${jwtSecret}`,
            `POSTGRES_PASSWORD=${postgresPassword}`,
            `REDIS_PASSWORD=${redisPassword}`,
            `GRAFANA_PASSWORD=${grafanaPassword}`,
            `ADMIN_EMAIL=${flags['admin-email']}`,
            `ADMIN_PASSWORD=${adminPassword}`,
        ].join('\n') + '\n', { mode: 0o600 });
        this.log('');
        this.log(chalk_1.default.bold('Resolved configuration'));
        this.log(`  cluster:     ${flags['cluster-name']} (${clusterId})`);
        this.log(`  base domain: ${baseDomain}`);
        this.log(`  api url:     https://api.${baseDomain}`);
        this.log(`  app url:     https://app.${baseDomain}`);
        this.log(`  admin:       ${flags['admin-email']}  (password saved to credentials.env)`);
        this.log(`  credentials: ${credPath}`);
        // Use the same TS-native script generator the cloud flow uses, so there
        // is exactly one definition of the bootstrap script in this CLI. The
        // service has a single zero-DB dependency (CliLoggerService), so we can
        // instantiate it directly instead of paying for a full Nest bootstrap.
        const k3sScriptService = new cli_k3s_script_service_1.CliK3sScriptService(new cli_logger_service_1.CliLoggerService());
        const bootstrapScript = await k3sScriptService.generateMasterScript({
            clusterId,
            clusterName: flags['cluster-name'],
            instanceId,
            instanceName,
            serverId,
            provider: 'byos',
            k3sToken,
            k3sVersion: flags['k3s-version'],
            deployObservabilityStack: flags.observability,
            postgresPassword,
            redisPassword,
            grafanaPassword,
            authMode: flags['auth-mode'],
            jwtSecret,
            adminEmail: flags['admin-email'],
            adminPassword,
            nipHostnameToken: nipToken,
            masterPublicIp: masterIp,
            // BYOS defaults: keep TLS off until the user wires a real DNS provider.
            nipIoCertEnabled: false,
            acmeStaging: false,
        });
        const localScriptPath = path.join(credentialsDir, 'bootstrap.sh');
        fs.writeFileSync(localScriptPath, bootstrapScript, { mode: 0o600 });
        if (flags['dry-run']) {
            this.log('');
            this.log(chalk_1.default.yellow(`--dry-run: bootstrap script written to ${localScriptPath}`));
            this.log(chalk_1.default.yellow('--dry-run: not touching remote host. Bye.'));
            return;
        }
        // ── Push the script to the target and execute it ──────────────────
        const remoteTmp = `/tmp/flui-byos-${clusterId}`;
        const sudo = flags.user === 'root' ? '' : 'sudo ';
        await this.runSsh(target, `${sudo}mkdir -p ${remoteTmp} && ${sudo}chmod 0700 ${remoteTmp}`);
        await this.runScp(target, localScriptPath, `${remoteTmp}/bootstrap.sh`);
        await this.runSsh(target, `${sudo}chmod 0700 ${remoteTmp}/bootstrap.sh`);
        this.log(chalk_1.default.cyan('\nLaunching bootstrap on remote (this can take several minutes)...\n'));
        const code = await this.streamSsh(target, `${sudo}bash ${remoteTmp}/bootstrap.sh 2>&1 | ${sudo}tee /var/log/flui-byos-install.log`);
        if (code !== 0) {
            this.error(`Remote installer exited with code ${code}`, { exit: 1 });
        }
        this.log('');
        this.log(chalk_1.default.green.bold('✓ Standalone install finished'));
        this.log(`  dashboard: https://app.${baseDomain}`);
        this.log(`  api:       https://api.${baseDomain}`);
        this.log(`  login:     ${flags['admin-email']} / ${adminPassword}`);
        this.log(chalk_1.default.dim(`  credentials saved to ${credPath}`));
    }
    printReport(r) {
        const tag = r.compatibility === 'supported'
            ? chalk_1.default.green('SUPPORTED')
            : r.compatibility === 'best-effort'
                ? chalk_1.default.yellow('BEST-EFFORT')
                : chalk_1.default.red('UNSUPPORTED');
        this.log('');
        this.log(chalk_1.default.bold('System compatibility'));
        this.log(`  os:           ${r.os.prettyName}      [${tag}]`);
        this.log(`  kernel:       ${r.kernel}`);
        this.log(`  arch:         ${r.arch}`);
        this.log(`  cpu:          ${r.cpu} cores`);
        this.log(`  memory:       ${r.memoryMb} MB`);
        this.log(`  free disk:    ${r.diskGb} GB on /`);
        this.log(`  systemd:      ${r.hasSystemd ? 'yes' : chalk_1.default.red('no')}`);
        this.log(`  curl:         ${r.hasCurl ? 'yes' : chalk_1.default.red('no')}`);
        this.log(`  ports free:   ${r.portsFree.map((p) => `${p.port}=${p.free ? '✓' : chalk_1.default.red('✗')}`).join('  ')}`);
        if (r.warnings.length > 0) {
            this.log('');
            this.log(chalk_1.default.yellow('warnings:'));
            for (const w of r.warnings)
                this.log(chalk_1.default.yellow(`  - ${w}`));
        }
        if (r.blockers.length > 0) {
            this.log('');
            this.log(chalk_1.default.red('blockers:'));
            for (const b of r.blockers)
                this.log(chalk_1.default.red(`  - ${b}`));
        }
    }
    async gateOnReport(r, bestEffortFlag) {
        if (r.blockers.length > 0) {
            this.log(chalk_1.default.red('\nThis machine is not eligible for Flui standalone install. Resolve blockers above first.'));
            return false;
        }
        if (r.compatibility === 'best-effort') {
            if (bestEffortFlag) {
                this.log(chalk_1.default.yellow('\nProceeding on a best-effort distro (--best-effort).'));
                return true;
            }
            this.log(chalk_1.default.yellow('\nThis distro is not in the tested matrix (Ubuntu 24.04+, Debian 12+).\n' +
                'You can either switch to a supported OS for a smooth install, or proceed best-effort.'));
            return await confirm('Proceed best-effort?');
        }
        return true;
    }
    async runSsh(target, command) {
        const r = (0, child_process_1.spawnSync)('ssh', [...sshOpts(target), `${target.user}@${target.host}`, command], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8' });
        if (r.status !== 0) {
            throw new Error(`ssh failed (${r.status}): ${r.stderr}`);
        }
    }
    async runScp(target, local, remote) {
        const r = (0, child_process_1.spawnSync)('scp', [
            '-i',
            target.privateKeyPath,
            '-o',
            'StrictHostKeyChecking=accept-new',
            '-o',
            'ConnectTimeout=10',
            local,
            `${target.user}@${target.host}:${remote}`,
        ], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8' });
        if (r.status !== 0) {
            throw new Error(`scp failed (${r.status}): ${r.stderr}`);
        }
    }
    streamSsh(target, command) {
        return new Promise((resolve) => {
            const child = (0, child_process_1.spawn)('ssh', [...sshOpts(target), `${target.user}@${target.host}`, command], { stdio: ['ignore', 'inherit', 'inherit'] });
            child.on('exit', (code) => resolve(code ?? 1));
        });
    }
}
StandaloneInstall.description = 'Install Flui (k3s + control plane + observability) on a virgin Linux machine over SSH (BYOS).';
StandaloneInstall.examples = [
    '<%= config.bin %> <%= command.id %> --host 1.2.3.4 --user ubuntu --ssh-key ~/.ssh/id_rsa',
    '<%= config.bin %> <%= command.id %> --host vps.example.com --user root --cluster-name flui-prod --observability',
    '<%= config.bin %> <%= command.id %> --host 10.0.0.5 --skip-precheck --best-effort',
];
StandaloneInstall.flags = {
    host: core_1.Flags.string({ required: true, description: 'Target host (IP or DNS)' }),
    user: core_1.Flags.string({ default: 'root', description: 'SSH user (sudo-able)' }),
    'ssh-key': core_1.Flags.string({
        default: path.join(os.homedir(), '.ssh', 'id_rsa'),
        description: 'Path to SSH private key',
    }),
    'cluster-name': core_1.Flags.string({ default: 'flui-byos' }),
    'cluster-id': core_1.Flags.string({ description: 'UUID; generated if omitted' }),
    'master-public-ip': core_1.Flags.string({
        description: 'Externally reachable IP (defaults to --host)',
    }),
    'auth-mode': core_1.Flags.string({
        options: ['local', 'oidc'],
        default: 'local',
    }),
    'admin-email': core_1.Flags.string({ default: 'admin@flui.local' }),
    'admin-password': core_1.Flags.string({ description: 'If omitted a random one is generated' }),
    observability: core_1.Flags.boolean({
        default: false,
        description: 'Deploy the observability stack on this cluster',
    }),
    'k3s-version': core_1.Flags.string({ default: DEFAULT_K3S_VERSION }),
    'skip-precheck': core_1.Flags.boolean({ default: false }),
    'best-effort': core_1.Flags.boolean({
        default: false,
        description: 'Proceed even on best-effort or unsupported distros without prompting',
    }),
    'dry-run': core_1.Flags.boolean({
        default: false,
        description: 'Show what would happen, do not touch the remote host',
    }),
};
exports.default = StandaloneInstall;
function sshOpts(target) {
    return [
        '-i',
        target.privateKeyPath,
        '-o',
        'StrictHostKeyChecking=accept-new',
        '-o',
        'ConnectTimeout=10',
        '-o',
        'BatchMode=yes',
        '-o',
        'ServerAliveInterval=30',
    ];
}
function randomPwd(len) {
    return crypto
        .randomBytes(Math.ceil((len * 3) / 4))
        .toString('base64')
        .replace(/[+/=]/g, '')
        .slice(0, len);
}
async function confirm(question) {
    process.stdout.write(`${question} [y/N] `);
    return new Promise((resolve) => {
        const onData = (chunk) => {
            const answer = chunk.toString().trim().toLowerCase();
            process.stdin.removeListener('data', onData);
            process.stdin.pause();
            resolve(answer === 'y' || answer === 'yes');
        };
        process.stdin.resume();
        process.stdin.once('data', onData);
    });
}
