"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_child_process_1 = require("node:child_process");
const node_util_1 = require("node:util");
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
const nest_app_1 = require("../../lib/nest-app");
const cli_observability_cluster_service_1 = require("../../services/cli-observability-cluster.service");
const cli_ssh_service_1 = require("../../services/cli-ssh.service");
const cluster_entity_1 = require("../../../../src/modules/infrastructure/clusters/entities/cluster.entity");
const FORWARDS = {
    postgres: {
        name: 'postgres',
        localPort: 5432,
        remotePort: 5432,
        service: 'svc/postgres',
        namespace: 'flui-system',
        needsKubectl: true,
    },
    redis: {
        name: 'redis',
        localPort: 6379,
        remotePort: 6379,
        service: 'svc/redis',
        namespace: 'flui-system',
        needsKubectl: true,
    },
    'kube-api': {
        name: 'kube-api',
        localPort: 6443,
        remotePort: 6443,
        needsKubectl: false,
    },
    grafana: {
        name: 'grafana',
        localPort: 3001,
        remotePort: 3000,
        service: 'svc/grafana',
        namespace: 'flui-observability',
        needsKubectl: true,
    },
    vmsingle: {
        name: 'vmsingle',
        localPort: 9090,
        remotePort: 8428,
        service: 'svc/vmsingle',
        namespace: 'flui-observability',
        needsKubectl: true,
    },
    loki: {
        name: 'loki',
        localPort: 3100,
        remotePort: 3100,
        service: 'svc/loki',
        namespace: 'flui-observability',
        needsKubectl: true,
    },
};
class DevTunnel extends core_1.Command {
    async run() {
        const { flags } = await this.parse(DevTunnel);
        const spinner = (0, ora_1.default)('Resolving cluster...').start();
        let masterIp;
        try {
            const app = await (0, nest_app_1.getNestApp)();
            const observabilityService = app.get(cli_observability_cluster_service_1.CliObservabilityClusterService);
            const cluster = await observabilityService.getObservabilityCluster();
            if (!cluster) {
                spinner.fail('No observability cluster found');
                console.log(chalk_1.default.yellow('\n⚠️  Run `flui env create` first.\n'));
                return;
            }
            if (cluster.status !== cluster_entity_1.ClusterStatus.READY) {
                spinner.fail(`Cluster not ready (status: ${cluster.status})`);
                return;
            }
            masterIp = cluster.masterIpAddress ?? undefined;
            if (!masterIp) {
                spinner.fail('Master IP address not available');
                return;
            }
            spinner.succeed(`Cluster ${cluster.name} → master ${masterIp}`);
        }
        catch (error) {
            spinner.fail('Error resolving cluster');
            console.error(chalk_1.default.red(`\n❌ ${error.message}\n`));
            this.exit(1);
            return;
        }
        const requested = flags.ports
            .split(',')
            .map((s) => s.trim())
            .filter(Boolean);
        const unknown = requested.filter((p) => !FORWARDS[p]);
        if (unknown.length > 0) {
            console.error(chalk_1.default.red(`\n❌ Unknown port(s): ${unknown.join(', ')}\n`));
            console.error(chalk_1.default.dim(`Available: ${Object.keys(FORWARDS).join(', ')}\n`));
            this.exit(1);
            return;
        }
        const specs = requested.map((p) => FORWARDS[p]);
        this.printForwardTable(specs);
        if (flags['kill-local']) {
            await this.freeLocalPorts(specs.map((s) => s.localPort));
        }
        const remoteCommand = this.buildRemoteCommand(specs);
        const sshForwards = specs.map((s) => ({
            localPort: s.localPort,
            remotePort: s.localPort, // remote side listens on same port via kubectl/loopback
        }));
        const app = await (0, nest_app_1.getNestApp)();
        const sshService = app.get(cli_ssh_service_1.CliSshService);
        let attempt = 0;
        let userExit = false;
        const onSigint = () => {
            userExit = true;
        };
        process.on('SIGINT', onSigint);
        try {
            while (!userExit) {
                attempt += 1;
                if (attempt > 1) {
                    console.log(chalk_1.default.yellow(`\n↻ Reconnecting (attempt ${attempt})...\n`));
                }
                else {
                    console.log(chalk_1.default.cyan(`\n🔌 Opening SSH tunnel to ${masterIp}. Press CTRL-C to close.\n`));
                }
                const result = await sshService.sshForward({
                    host: masterIp,
                    forwards: sshForwards,
                    remoteCommand,
                    expectedForwardLines: specs.filter((s) => s.needsKubectl).length,
                    onReady: () => {
                        console.log(chalk_1.default.green('\n✅ Tunnel up. Localhost endpoints:\n'));
                        for (const s of specs) {
                            console.log(`   ${s.name.padEnd(11)} → 127.0.0.1:${s.localPort}`);
                        }
                        console.log();
                    },
                });
                if (userExit || !flags.retry)
                    break;
                console.log(chalk_1.default.yellow(`SSH session ended (status=${result.status}, signal=${result.signal}).`));
                // small backoff
                await new Promise((r) => setTimeout(r, 2000));
            }
        }
        finally {
            process.off('SIGINT', onSigint);
            await (0, nest_app_1.closeNestApp)();
        }
    }
    async freeLocalPorts(ports) {
        for (const port of ports) {
            const pids = await this.findListeningPids(port);
            if (pids.length === 0)
                continue;
            const ownPid = process.pid;
            const targets = pids.filter((p) => p !== ownPid);
            if (targets.length === 0)
                continue;
            console.log(chalk_1.default.yellow(`⚠️  Port ${port} in use by pid(s) ${targets.join(', ')} — killing...`));
            for (const pid of targets) {
                try {
                    process.kill(pid, 'SIGTERM');
                }
                catch {
                    /* already gone */
                }
            }
            await new Promise((r) => setTimeout(r, 400));
            const stillThere = (await this.findListeningPids(port)).filter((p) => p !== ownPid);
            for (const pid of stillThere) {
                try {
                    process.kill(pid, 'SIGKILL');
                }
                catch {
                    /* already gone */
                }
            }
        }
    }
    async findListeningPids(port) {
        try {
            const { stdout } = await execFileAsync('lsof', [
                '-ti',
                `tcp:${port}`,
                '-sTCP:LISTEN',
            ]);
            return stdout
                .split('\n')
                .map((s) => s.trim())
                .filter(Boolean)
                .map((s) => Number.parseInt(s, 10))
                .filter((n) => Number.isFinite(n));
        }
        catch {
            return [];
        }
    }
    printForwardTable(specs) {
        console.log(chalk_1.default.cyan('\n📡 Forwards to open:\n'));
        for (const s of specs) {
            const target = s.service
                ? `${s.namespace}/${s.service}:${s.remotePort}`
                : `master:127.0.0.1:${s.remotePort}`;
            console.log(`   ${s.name.padEnd(11)} 127.0.0.1:${s.localPort}  →  ${target}`);
        }
    }
    buildRemoteCommand(specs) {
        const kubectlSpecs = specs.filter((s) => s.needsKubectl);
        if (kubectlSpecs.length === 0) {
            // Nothing to run remotely; SSH -N is enough. The kube-API is reached via
            // the local-forward to 127.0.0.1:6443 on the master (kube-API binds there).
            return undefined;
        }
        // Use newlines as separators: `&` terminates a command on its own, so
        // joining with `; ` would emit invalid `&;` sequences. Trap HUP too so
        // we clean up child kubectl processes when SSH disconnects.
        const portsToFree = kubectlSpecs.map((s) => s.localPort).join(' ');
        const lines = [
            // Pre-cleanup: free the loopback ports we want to bind. fuser -k is
            // port-targeted (won't match our own sh wrapper by accident); pkill
            // is a fallback if fuser is not installed.
            `for p in ${portsToFree}; do fuser -k -n tcp $p 2>/dev/null || true; done`,
            'pkill -f "^kubectl .*port-forward" 2>/dev/null || true',
            'sleep 1',
            'trap "kill 0" INT TERM EXIT HUP',
        ];
        for (const s of kubectlSpecs) {
            lines.push(`kubectl -n ${s.namespace} port-forward --address 127.0.0.1 ${s.service} ${s.localPort}:${s.remotePort} &`);
        }
        lines.push('wait');
        return `sh -c '${lines.join('\n')}'`;
    }
}
DevTunnel.description = 'Open SSH tunnels from localhost to the observability cluster services.\n' +
    'On the remote side runs kubectl port-forward against the in-cluster Services,\n' +
    'so no NodePort or kube-API exposure is required. Stay in foreground; CTRL-C to close.';
DevTunnel.examples = [
    '<%= config.bin %> <%= command.id %>',
    '<%= config.bin %> <%= command.id %> --ports postgres,redis',
    '<%= config.bin %> <%= command.id %> --ports postgres,redis,kube-api,grafana',
    '<%= config.bin %> <%= command.id %> --no-retry',
];
DevTunnel.flags = {
    ports: core_1.Flags.string({
        description: 'Comma-separated list of forwards to open. ' +
            `Available: ${Object.keys(FORWARDS).join(', ')}`,
        default: 'postgres,redis,kube-api,vmsingle,loki',
    }),
    retry: core_1.Flags.boolean({
        description: 'Reconnect automatically if the SSH session drops. Disable with --no-retry.',
        default: true,
        allowNo: true,
    }),
    'kill-local': core_1.Flags.boolean({
        description: 'Kill any local process bound to the requested ports before opening the tunnel. Disable with --no-kill-local.',
        default: true,
        allowNo: true,
    }),
};
exports.default = DevTunnel;
