"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 (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DockerExecutor = void 0;
const child_process_1 = require("child_process");
const path = __importStar(require("path"));
const docker_assets_1 = require("./docker-assets");
const LOCAL_DOCKER_SYNC_TIMEOUT_MS = 10000;
const REMOTE_DOCKER_SYNC_TIMEOUT_MS = 30000;
const REMOTE_DOCKER_BUILD_TIMEOUT_MS = 300000;
function quotePosix(value) {
    return `'${value.replace(/'/g, `'\"'\"'`)}'`;
}
function renderCommand(command, args) {
    return [command, ...args.map((arg) => quotePosix(arg))].join(' ');
}
function normalizeOutput(value) {
    if (typeof value === 'string')
        return value;
    return value ? value.toString('utf8') : '';
}
function formatTimeout(timeoutMs) {
    return timeoutMs % 1000 === 0 ? `${timeoutMs / 1000}s` : `${timeoutMs}ms`;
}
function buildTimeoutMessage(command, args, timeoutMs, remote) {
    return [
        `Command timed out after ${formatTimeout(timeoutMs)} while running a ${remote ? 'remote' : 'local'} Docker command.`,
        `Command: ${renderCommand(command, args)}`,
        remote
            ? 'Check SSH reachability and the remote Docker host, then try again.'
            : 'Check Docker availability on this machine, then try again.',
    ].join('\n');
}
function applyDefaultTimeouts(options) {
    return {
        ...options,
        timeoutMs: options.timeoutMs ??
            (options.remote ? REMOTE_DOCKER_SYNC_TIMEOUT_MS : LOCAL_DOCKER_SYNC_TIMEOUT_MS),
    };
}
function runSync(command, args, options = {}) {
    const normalizedOptions = applyDefaultTimeouts(options);
    const result = (0, child_process_1.spawnSync)(command, args, {
        cwd: normalizedOptions.cwd,
        env: normalizedOptions.env,
        encoding: 'utf8',
        stdio: 'pipe',
        timeout: normalizedOptions.timeoutMs,
        windowsHide: true,
    });
    const errorMessage = result.error && 'code' in result.error && result.error.code === 'ETIMEDOUT'
        ? buildTimeoutMessage(command, args, normalizedOptions.timeoutMs ?? LOCAL_DOCKER_SYNC_TIMEOUT_MS, normalizedOptions.remote ?? false)
        : result.error?.message;
    const stderr = [normalizeOutput(result.stderr), errorMessage].filter(Boolean).join('\n');
    return {
        command: renderCommand(command, args),
        exitCode: result.status ?? 1,
        stdout: normalizeOutput(result.stdout),
        stderr,
        remote: normalizedOptions.remote ?? false,
    };
}
function runStreaming(command, args) {
    return new Promise((resolve, reject) => {
        const child = (0, child_process_1.spawn)(command, args, {
            stdio: 'inherit',
            windowsHide: true,
        });
        child.on('error', reject);
        child.on('close', (code) => {
            if ((code ?? 1) === 0) {
                resolve();
                return;
            }
            reject(new Error(`Command failed (${code ?? 1}): ${renderCommand(command, args)}`));
        });
    });
}
function sleepSync(ms) {
    const buffer = new SharedArrayBuffer(4);
    Atomics.wait(new Int32Array(buffer), 0, 0, ms);
}
let cachedLocalComposePrefix;
function resolveLocalComposePrefix() {
    if (cachedLocalComposePrefix) {
        return [...cachedLocalComposePrefix];
    }
    if (runSync('docker', ['compose', 'version'], {
        timeoutMs: LOCAL_DOCKER_SYNC_TIMEOUT_MS,
    }).exitCode === 0) {
        cachedLocalComposePrefix = ['docker', 'compose'];
        return [...cachedLocalComposePrefix];
    }
    if (runSync('docker-compose', ['version'], {
        timeoutMs: LOCAL_DOCKER_SYNC_TIMEOUT_MS,
    }).exitCode === 0) {
        cachedLocalComposePrefix = ['docker-compose'];
        return [...cachedLocalComposePrefix];
    }
    throw new Error('Docker Compose is not available. Install Docker Desktop or docker-compose.');
}
function buildRemoteComposeCommand(composeArgs) {
    const suffix = composeArgs.map((arg) => quotePosix(arg)).join(' ');
    return [
        'if docker compose version >/dev/null 2>&1; then',
        `docker compose ${suffix};`,
        'elif docker-compose version >/dev/null 2>&1; then',
        `docker-compose ${suffix};`,
        'else',
        "echo 'Docker Compose is not available on the remote host.' >&2;",
        'exit 127;',
        'fi',
    ].join(' ');
}
function buildRemoteDockerCommand(args) {
    return ['docker', ...args.map((arg) => quotePosix(arg))].join(' ');
}
class DockerExecutor {
    constructor(deps = {}) {
        this.deps = deps;
        this.assets = deps.assets ?? (0, docker_assets_1.getDockerAssetPaths)();
    }
    getConfig(options) {
        return (0, docker_assets_1.createDockerConfigSummary)(options);
    }
    up(options) {
        if (options.host) {
            this.stageRemoteAssets(options.host);
        }
        this.ensureSuccess(this.runCompose(['up', '-d', '--build'], options, {
            CCS_NPM_VERSION: this.getInstalledCcsVersion(),
            CCS_DASHBOARD_PORT: String(options.port),
            CCS_CLIPROXY_PORT: String(options.proxyPort),
            CCS_DOCKER_BIND_HOST: process.env.CCS_DOCKER_BIND_HOST || '127.0.0.1',
            CCS_DOCKER_LEGACY_KEY_GRACE_DAYS: process.env.CCS_DOCKER_LEGACY_KEY_GRACE_DAYS || '',
            CCS_DOCKER_RESTORE_LEGACY_API_KEY: process.env.CCS_DOCKER_RESTORE_LEGACY_API_KEY || '',
        }, REMOTE_DOCKER_BUILD_TIMEOUT_MS), 'Docker stack startup', options);
    }
    down(options) {
        this.ensureSuccess(this.runCompose(['down'], options), 'Docker stack shutdown', options);
    }
    status(options) {
        const compose = this.runCompose(['ps'], options);
        this.ensureSuccess(compose, 'Docker status', options);
        let supervisor;
        if (compose.exitCode === 0) {
            supervisor = this.runDocker(['exec', docker_assets_1.DOCKER_CONTAINER_NAME, 'supervisorctl', '-c', '/etc/supervisord.conf', 'status'], options);
        }
        return { compose, supervisor };
    }
    update(options) {
        const script = 'npm install -g @kaitranntt/ccs@latest --force && ccs cliproxy --latest && supervisorctl -c /etc/supervisord.conf restart ccs-dashboard cliproxy';
        this.ensureSuccess(this.runDocker(['exec', docker_assets_1.DOCKER_CONTAINER_NAME, 'sh', '-lc', script], options, REMOTE_DOCKER_BUILD_TIMEOUT_MS), 'Docker stack update', options);
    }
    async logs(options) {
        const files = options.service
            ? [docker_assets_1.DOCKER_LOG_FILES[options.service]]
            : [docker_assets_1.DOCKER_LOG_FILES.ccs, docker_assets_1.DOCKER_LOG_FILES.cliproxy];
        const touch = `mkdir -p /var/log/ccs && touch ${files.map((file) => quotePosix(file)).join(' ')}`;
        const command = options.follow
            ? `${touch} && tail -n 100 -F ${files.map((file) => quotePosix(file)).join(' ')}`
            : options.service
                ? `${touch} && tail -n 100 ${quotePosix(files[0])}`
                : `${touch} && printf '== ccs ==\\n' && tail -n 100 ${quotePosix(docker_assets_1.DOCKER_LOG_FILES.ccs)} && printf '\\n== cliproxy ==\\n' && tail -n 100 ${quotePosix(docker_assets_1.DOCKER_LOG_FILES.cliproxy)}`;
        if (options.follow) {
            await this.runDockerStreaming(['exec', docker_assets_1.DOCKER_CONTAINER_NAME, 'sh', '-lc', command], options);
            return;
        }
        const result = this.runDocker(['exec', docker_assets_1.DOCKER_CONTAINER_NAME, 'sh', '-lc', command], options);
        this.ensureSuccess(result, 'Docker log retrieval', options);
        return result.stdout;
    }
    showKey(options, full) {
        const args = [
            'exec',
            docker_assets_1.DOCKER_CONTAINER_NAME,
            'ccs',
            'docker',
            'show-key',
            '--container-scope',
            ...(full ? ['--full'] : []),
        ];
        const result = this.runDocker(args, options);
        this.ensureSuccess(result, 'Docker key display', options);
        return result.stdout;
    }
    finalizeKeyRotation(options) {
        const result = this.runDocker([
            'exec',
            docker_assets_1.DOCKER_CONTAINER_NAME,
            'ccs',
            'docker',
            'finalize-key-rotation',
            '--container-scope',
        ], options);
        this.ensureSuccess(result, 'Docker key rotation finalization', options);
        return result.stdout;
    }
    getKeyRotationBanner(options) {
        const args = [
            'exec',
            docker_assets_1.DOCKER_CONTAINER_NAME,
            'ccs',
            'docker',
            'show-key',
            '--container-scope',
            '--full',
            '--banner-only',
        ];
        for (let attempt = 0; attempt < 10; attempt += 1) {
            const result = this.runDocker(args, options, LOCAL_DOCKER_SYNC_TIMEOUT_MS);
            if (result.exitCode === 0) {
                return result.stdout.trim();
            }
            sleepSync(500);
        }
        return '';
    }
    stageRemoteAssets(host) {
        this.ensureSuccess(this.runSync('ssh', [host, `mkdir -p ${docker_assets_1.DOCKER_REMOTE_DIR}`], { remote: true }), 'Remote Docker asset staging', { host });
        const files = [
            this.assets.composeFile,
            this.assets.dockerfile,
            this.assets.supervisordConfig,
            this.assets.entrypoint,
        ];
        const target = `${host}:${docker_assets_1.DOCKER_REMOTE_DIR}/`;
        this.ensureSuccess(this.runSync('scp', [...files, target], { remote: true }), 'Remote Docker asset copy', {
            host,
        });
    }
    runCompose(args, options, env = {}, timeoutMs) {
        if (!options.host) {
            const prefix = this.resolveLocalComposePrefix();
            const command = prefix[0];
            const composeArgs = [...prefix.slice(1), '-f', this.assets.composeFile, ...args];
            return this.runSync(command, composeArgs, {
                cwd: path.dirname(this.assets.composeFile),
                env: { ...process.env, ...env },
                remote: false,
                timeoutMs,
            });
        }
        const envExports = Object.entries(env)
            .map(([key, value]) => `export ${key}=${quotePosix(value)}`)
            .join(' && ');
        const composeArgs = ['-f', path.basename(this.assets.composeFile), ...args];
        const remoteCommand = `cd ${docker_assets_1.DOCKER_REMOTE_DIR}${envExports ? ` && ${envExports}` : ''} && ${buildRemoteComposeCommand(composeArgs)}`;
        return this.runSync('ssh', [options.host, remoteCommand], { remote: true, timeoutMs });
    }
    runDocker(args, options, timeoutMs) {
        if (!options.host) {
            return this.runSync('docker', args, { timeoutMs });
        }
        return this.runSync('ssh', [options.host, buildRemoteDockerCommand(args)], {
            remote: true,
            timeoutMs,
        });
    }
    async runDockerStreaming(args, options) {
        if (!options.host) {
            await this.runStreaming('docker', args);
            return;
        }
        await this.runStreaming('ssh', [options.host, buildRemoteDockerCommand(args)]);
    }
    getInstalledCcsVersion() {
        return this.deps.getInstalledCcsVersion?.() ?? (0, docker_assets_1.getInstalledCcsVersion)();
    }
    resolveLocalComposePrefix() {
        return this.deps.resolveLocalComposePrefix?.() ?? resolveLocalComposePrefix();
    }
    runSync(command, args, options = {}) {
        const normalizedOptions = applyDefaultTimeouts(options);
        return (this.deps.runSync?.(command, args, normalizedOptions) ??
            runSync(command, args, normalizedOptions));
    }
    async runStreaming(command, args) {
        await (this.deps.runStreaming?.(command, args) ?? runStreaming(command, args));
    }
    ensureSuccess(result, label, options) {
        if (result.exitCode === 0) {
            return;
        }
        const detail = (result.stderr || result.stdout).trim();
        const hint = options.host && /No such file|no configuration file|can't cd|not found/i.test(detail)
            ? `\nRun \`ccs docker up --host ${options.host}\` first.`
            : '';
        throw new Error(`${label} failed.${detail ? `\n${detail}` : ''}${hint}`);
    }
}
exports.DockerExecutor = DockerExecutor;
//# sourceMappingURL=docker-executor.js.map