"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 __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
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 __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var CliSshService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CliSshService = void 0;
const common_1 = require("@nestjs/common");
const fs = __importStar(require("node:fs"));
const path = __importStar(require("node:path"));
const os = __importStar(require("node:os"));
const node_child_process_1 = require("node:child_process");
const cli_ca_service_1 = require("./cli-ca.service");
/**
 * CLI SSH Management Service
 *
 * Manages SSH access with ephemeral certificates:
 * - Generates ephemeral ED25519 keypair for each connection
 * - Gets certificate signed by CA (5-minute validity)
 * - Uses certificate for SSH authentication
 * - Cleans up temporary files after connection
 */
let CliSshService = CliSshService_1 = class CliSshService {
    constructor(caService) {
        this.caService = caService;
        this.logger = new common_1.Logger(CliSshService_1.name);
        this.fluiDir = path.join(os.homedir(), '.flui');
        this.sshDir = path.join(this.fluiDir, 'ssh');
        this.privateKeyPath = path.join(this.sshDir, 'id_rsa');
        this.publicKeyPath = path.join(this.sshDir, 'id_rsa.pub');
        this.ensureSshDir();
    }
    /**
     * Ensure ~/.flui/ssh directory exists
     */
    ensureSshDir() {
        if (!fs.existsSync(this.sshDir)) {
            fs.mkdirSync(this.sshDir, { recursive: true, mode: 0o700 });
            this.logger.log(`Created SSH directory: ${this.sshDir}`);
        }
    }
    /**
     * Get or generate SSH key pair
     * Returns the public key content for server provisioning
     */
    async getOrCreateSshKey() {
        // Check if key already exists
        if (fs.existsSync(this.privateKeyPath) &&
            fs.existsSync(this.publicKeyPath)) {
            this.logger.debug('Using existing SSH key');
            const publicKey = fs.readFileSync(this.publicKeyPath, 'utf-8').trim();
            return {
                publicKey,
                privateKeyPath: this.privateKeyPath,
                publicKeyPath: this.publicKeyPath,
            };
        }
        // Generate new SSH key pair
        this.logger.log('Generating new SSH key pair for Flui CLI...');
        try {
            // Use ssh-keygen to generate key pair
            (0, node_child_process_1.spawnSync)('ssh-keygen', [
                '-t',
                'rsa',
                '-b',
                '4096',
                '-f',
                this.privateKeyPath,
                '-N',
                '',
                '-C',
                `flui-cli@${os.hostname()}`,
            ], { stdio: 'pipe' });
            // Set correct permissions
            fs.chmodSync(this.privateKeyPath, 0o600);
            fs.chmodSync(this.publicKeyPath, 0o644);
            const publicKey = fs.readFileSync(this.publicKeyPath, 'utf-8').trim();
            this.logger.log('SSH key pair generated successfully');
            this.logger.log(`Private key: ${this.privateKeyPath}`);
            this.logger.log(`Public key: ${this.publicKeyPath}`);
            return {
                publicKey,
                privateKeyPath: this.privateKeyPath,
                publicKeyPath: this.publicKeyPath,
            };
        }
        catch (error) {
            this.logger.error('Failed to generate SSH key:', error);
            throw new Error(`SSH key generation failed: ${error.message}`);
        }
    }
    /**
     * Get SSH public key content
     */
    async getPublicKey() {
        const { publicKey } = await this.getOrCreateSshKey();
        return publicKey;
    }
    /**
     * Get SSH private key path for SSH connections
     */
    getPrivateKeyPath() {
        if (!fs.existsSync(this.privateKeyPath)) {
            throw new Error('SSH private key not found. Run getOrCreateSshKey() first.');
        }
        return this.privateKeyPath;
    }
    /**
     * Generate ephemeral keypair and get signed certificate
     * Returns paths to private key and certificate
     */
    async generateEphemeralKeypair() {
        const tempDir = path.join(os.tmpdir(), `flui-ephemeral-${Date.now()}`);
        fs.mkdirSync(tempDir, { mode: 0o700 });
        const privateKeyPath = path.join(tempDir, 'ephemeral_key');
        const publicKeyPath = `${privateKeyPath}.pub`;
        const certificatePath = `${privateKeyPath}-cert.pub`;
        this.logger.debug('Generating ephemeral SSH keypair...');
        // Generate ED25519 keypair
        (0, node_child_process_1.spawnSync)('ssh-keygen', ['-t', 'ed25519', '-f', privateKeyPath, '-N', '', '-C', 'flui-ephemeral'], { stdio: 'pipe' });
        // Set permissions
        fs.chmodSync(privateKeyPath, 0o600);
        fs.chmodSync(publicKeyPath, 0o644);
        // Read public key
        const publicKey = fs.readFileSync(publicKeyPath, 'utf-8').trim();
        // Get certificate signed by CA (5-minute validity)
        this.logger.debug('Signing ephemeral key with CA...');
        const certificate = await this.caService.signPublicKey(publicKey, 300);
        // Write certificate
        fs.writeFileSync(certificatePath, certificate, { mode: 0o644 });
        this.logger.debug(`Ephemeral certificate created: ${certificatePath}`);
        return {
            privateKeyPath,
            publicKeyPath,
            certificatePath,
            cleanup: () => {
                try {
                    fs.rmSync(tempDir, { recursive: true, force: true });
                    this.logger.debug(`Cleaned up ephemeral keys: ${tempDir}`);
                }
                catch {
                    this.logger.warn(`Failed to cleanup ephemeral keys: ${tempDir}`);
                }
            },
        };
    }
    /**
     * SSH into a server using ephemeral certificate
     */
    async sshConnect(host, username = 'root') {
        const { privateKeyPath, certificatePath, cleanup } = await this.generateEphemeralKeypair();
        this.logger.log(`Connecting to ${username}@${host} with ephemeral certificate...`);
        try {
            const result = (0, node_child_process_1.spawnSync)('ssh', [
                '-i',
                privateKeyPath,
                '-o',
                `CertificateFile=${certificatePath}`,
                '-o',
                'StrictHostKeyChecking=no',
                '-o',
                'UserKnownHostsFile=/dev/null',
                '-o',
                'PasswordAuthentication=no',
                '-o',
                'PubkeyAuthentication=yes',
                '-o',
                'PreferredAuthentications=publickey',
                `${username}@${host}`,
            ], { stdio: 'inherit' });
            // 0 = clean exit, 130 = Ctrl+C (SIGINT) — both are graceful
            const isGraceful = result.status === 0 ||
                result.status === 130 ||
                result.signal === 'SIGINT';
            if (!isGraceful) {
                throw new Error(`SSH exited with code ${result.status ?? result.signal}`);
            }
        }
        finally {
            cleanup();
        }
    }
    /**
     * Execute command on remote server via SSH with ephemeral certificate
     */
    async sshExec(host, command, username = 'root') {
        const { privateKeyPath, certificatePath, cleanup } = await this.generateEphemeralKeypair();
        try {
            const result = (0, node_child_process_1.spawnSync)('ssh', [
                '-i',
                privateKeyPath,
                '-o',
                `CertificateFile=${certificatePath}`,
                '-o',
                'StrictHostKeyChecking=no',
                '-o',
                'UserKnownHostsFile=/dev/null',
                '-o',
                'PasswordAuthentication=no',
                '-o',
                'PubkeyAuthentication=yes',
                '-o',
                'PreferredAuthentications=publickey',
                '-o',
                'BatchMode=yes',
                '-o',
                'ConnectTimeout=10',
                '-o',
                'ServerAliveInterval=5',
                '-o',
                'ServerAliveCountMax=2',
                `${username}@${host}`,
                command,
            ], { encoding: 'utf-8', timeout: 30_000 });
            if (result.error &&
                result.error.code === 'ETIMEDOUT') {
                throw new Error(`SSH command timed out after 30s on ${host}`);
            }
            if (result.status !== 0) {
                const errMsg = result.stderr?.trim() || `SSH exited with code ${result.status}`;
                throw new Error(`Command failed: ${errMsg}`);
            }
            return result.stdout.trim();
        }
        finally {
            cleanup();
        }
    }
    /**
     * Open an SSH session to `host` that requests one or more local port
     * forwards (`-L localPort:remoteHost:remotePort`) and runs `remoteCommand`
     * on the remote side. Stays in foreground until the user kills it or the
     * remote command exits. Returns the child's exit info.
     */
    async sshForward(opts) {
        const username = opts.username ?? 'root';
        const { privateKeyPath, certificatePath, cleanup } = await this.generateEphemeralKeypair();
        const args = [
            '-i',
            privateKeyPath,
            '-o',
            `CertificateFile=${certificatePath}`,
            '-o',
            'StrictHostKeyChecking=no',
            '-o',
            'UserKnownHostsFile=/dev/null',
            '-o',
            'PasswordAuthentication=no',
            '-o',
            'PubkeyAuthentication=yes',
            '-o',
            'PreferredAuthentications=publickey',
            '-o',
            'ServerAliveInterval=30',
            '-o',
            'ServerAliveCountMax=3',
            '-o',
            'ExitOnForwardFailure=yes',
        ];
        for (const f of opts.forwards) {
            const remoteHost = f.remoteHost ?? '127.0.0.1';
            args.push('-L', `${f.localPort}:${remoteHost}:${f.remotePort}`);
        }
        if (!opts.remoteCommand) {
            args.push('-N');
        }
        args.push(`${username}@${opts.host}`);
        if (opts.remoteCommand) {
            args.push(opts.remoteCommand);
        }
        return new Promise((resolve) => {
            const child = (0, node_child_process_1.spawn)('ssh', args, { stdio: ['ignore', 'pipe', 'pipe'] });
            const onSignal = (signal) => {
                if (!child.killed)
                    child.kill(signal);
            };
            process.on('SIGINT', onSignal);
            process.on('SIGTERM', onSignal);
            // Forward output to terminal AND watch for the kubectl readiness marker
            // ("Forwarding from 127.0.0.1:<port>"). Fire onReady once we have seen
            // as many such lines as kubectl forwards we requested.
            let readyFired = false;
            let forwardingLinesSeen = 0;
            const expected = opts.expectedForwardLines ?? 0;
            const watchStream = (stream, sink) => {
                let buffer = '';
                stream.on('data', (chunk) => {
                    sink.write(chunk);
                    buffer += chunk.toString('utf-8');
                    let idx;
                    while ((idx = buffer.indexOf('\n')) !== -1) {
                        const line = buffer.slice(0, idx);
                        buffer = buffer.slice(idx + 1);
                        if (!readyFired &&
                            expected > 0 &&
                            /Forwarding from 127\.0\.0\.1:\d+/.test(line)) {
                            forwardingLinesSeen += 1;
                            if (forwardingLinesSeen >= expected) {
                                readyFired = true;
                                opts.onReady?.();
                            }
                        }
                    }
                });
            };
            if (child.stdout)
                watchStream(child.stdout, process.stdout);
            if (child.stderr)
                watchStream(child.stderr, process.stderr);
            // Fallback for the no-kubectl case (e.g. pure kube-api forward): no
            // readiness marker on stderr, declare ready after a short grace period.
            let readyTimer;
            if (opts.onReady && expected === 0) {
                readyTimer = setTimeout(() => {
                    if (!readyFired && child.exitCode === null) {
                        readyFired = true;
                        opts.onReady?.();
                    }
                }, 1500);
            }
            child.on('exit', (status, signal) => {
                if (readyTimer)
                    clearTimeout(readyTimer);
                process.off('SIGINT', onSignal);
                process.off('SIGTERM', onSignal);
                cleanup();
                resolve({ status, signal });
            });
        });
    }
    /**
     * Get remote log file via SSH
     */
    async getRemoteLog(host, logPath, username = 'root') {
        return this.sshExec(host, `cat '${logPath}'`, username);
    }
    /**
     * Tail remote log file via SSH
     */
    async tailRemoteLog(host, logPath, lines = 100, username = 'root') {
        return this.sshExec(host, `tail -n ${lines} '${logPath}'`, username);
    }
    /**
     * Stream remote log file via SSH with tail -f
     * Returns a cleanup function that should be called to stop streaming and cleanup resources
     */
    async streamRemoteLog(host, logPath, username = 'root', onData) {
        const { privateKeyPath, certificatePath, cleanup: cleanupKeys, } = await this.generateEphemeralKeypair();
        this.logger.debug(`Starting log stream from ${username}@${host}:${logPath}`);
        // Spawn SSH process with tail -f
        const sshProcess = (0, node_child_process_1.spawn)('ssh', [
            '-i',
            privateKeyPath,
            '-o',
            `CertificateFile=${certificatePath}`,
            '-o',
            'StrictHostKeyChecking=no',
            '-o',
            'UserKnownHostsFile=/dev/null',
            '-o',
            'PasswordAuthentication=no',
            '-o',
            'PubkeyAuthentication=yes',
            '-o',
            'PreferredAuthentications=publickey',
            '-o',
            'BatchMode=yes',
            `${username}@${host}`,
            `tail -f ${logPath}`,
        ], {
            stdio: ['pipe', 'pipe', 'pipe'],
            windowsHide: true,
        });
        // Forward stdout to callback or console
        sshProcess.stdout.on('data', (data) => {
            const text = data.toString();
            if (onData) {
                onData(text);
            }
            else {
                process.stdout.write(text);
            }
        });
        // Forward stderr to console
        sshProcess.stderr.on('data', (data) => {
            process.stderr.write(data);
        });
        // Handle process errors
        sshProcess.on('error', (error) => {
            this.logger.error(`SSH stream process error: ${error.message}`);
        });
        // Cleanup function to kill process and cleanup keys
        const cleanup = () => {
            if (!sshProcess.killed) {
                this.logger.debug('Stopping log stream...');
                sshProcess.kill('SIGTERM');
                // Force kill after 2 seconds if still running
                setTimeout(() => {
                    if (!sshProcess.killed) {
                        sshProcess.kill('SIGKILL');
                    }
                }, 2000);
            }
            cleanupKeys();
        };
        // Cleanup on process exit
        sshProcess.on('exit', (code, signal) => {
            this.logger.debug(`SSH stream process exited with code ${code}, signal ${signal}`);
            cleanupKeys();
        });
        return { cleanup };
    }
};
exports.CliSshService = CliSshService;
exports.CliSshService = CliSshService = CliSshService_1 = __decorate([
    (0, common_1.Injectable)(),
    __metadata("design:paramtypes", [cli_ca_service_1.CliCaService])
], CliSshService);
