import fs from "node:fs";
import path from "node:path";
import { spawn } from "node:child_process";
import { getAppliedTunnelCredential, probeLocalTunnelCredential } from "./runtime-credentials.js";
import { getProfileTunnelConfigPath, getProfileTunnelDir, getProfileTunnelCredentialsPath } from "../config/index.js";
import { writeProfileTunnelConfig } from "../obs/cloudflared-config.js";
import { logger } from "../logger.js";
export const TUNNEL_ROTATION_CAPABILITIES = [
    "tunnel.rotation.blue_green.v1",
    "tunnel.rotation.prepare.v1",
    "tunnel.rotation.status_ack.v1",
    "tunnel.rotation.manifest.v1",
    "tunnel.rotation.finalize.v1",
    "tunnel.rotation.rollback.v1",
    "tunnel.multi_connector.v1",
];
function manifestPath() {
    return path.join(getProfileTunnelDir(), "rotation-manifest.json");
}
function pendingConfigPath(operationId) {
    return path.join(getProfileTunnelDir(), "rotation", operationId, "config.yml");
}
function readManifest() {
    try {
        const parsed = JSON.parse(fs.readFileSync(manifestPath(), "utf-8"));
        return typeof parsed.operationId === "string" ? parsed : undefined;
    }
    catch {
        return undefined;
    }
}
function writeManifest(manifest) {
    fs.mkdirSync(path.dirname(manifestPath()), { recursive: true });
    fs.writeFileSync(manifestPath(), JSON.stringify(manifest, null, 2), "utf-8");
}
function removeManifest() {
    try {
        fs.rmSync(manifestPath(), { force: true });
    }
    catch {
        // best effort
    }
}
function isProcessAlive(pid) {
    if (!pid)
        return false;
    try {
        process.kill(pid, 0);
        return true;
    }
    catch {
        return false;
    }
}
function stopPid(pid) {
    if (!pid || !isProcessAlive(pid))
        return;
    try {
        process.kill(pid, "SIGTERM");
    }
    catch {
        return;
    }
    setTimeout(() => {
        if (!isProcessAlive(pid))
            return;
        try {
            process.kill(pid, "SIGKILL");
        }
        catch {
            // already gone
        }
    }, 3_000).unref();
}
function resolveObsPort(port) {
    return typeof port === "number" && Number.isFinite(port) && port > 0 ? port : 3101;
}
export class TunnelRotationManager {
    obsPort;
    restartTunnel;
    status;
    pendingProcess;
    pendingOperationId;
    constructor(options = {}) {
        this.obsPort = resolveObsPort(options.obsPort);
        this.restartTunnel = options.restartTunnel;
    }
    get currentStatus() {
        return this.status;
    }
    getRuntimeStatus() {
        try {
            const applied = getAppliedTunnelCredential();
            const probed = applied ?? probeLocalTunnelCredential();
            return probed
                ? {
                    currentTunnelId: probed.tunnelId,
                    currentConnectorManagedByProcess: true,
                    currentConnectorSource: "runtime_current",
                }
                : undefined;
        }
        catch {
            return undefined;
        }
    }
    handle(command) {
        if (!command)
            return;
        if (command.phase === "prepare") {
            this.handlePrepare(command);
            return;
        }
        if (command.phase === "finalize") {
            this.handleFinalize(command);
            return;
        }
        if (command.phase === "rollback") {
            this.handleRollback(command);
            return;
        }
        this.handleCleanup(command);
    }
    handlePrepare(command) {
        if (!command.pending) {
            this.report(command.operationId, "pending_failed", command.oldTunnelId, undefined, "Missing pending credential");
            return;
        }
        const manifest = readManifest();
        if (manifest?.operationId === command.operationId &&
            manifest.pendingPid &&
            isProcessAlive(manifest.pendingPid)) {
            this.report(command.operationId, "pending_started", command.oldTunnelId, command.pending.tunnelId);
            return;
        }
        if (this.pendingProcess && this.pendingOperationId !== command.operationId) {
            void this.stopPendingProcess();
        }
        try {
            const tunnelDir = getProfileTunnelDir();
            const credentialsFile = getProfileTunnelCredentialsPath(command.pending.tunnelId);
            const configFile = pendingConfigPath(command.operationId);
            fs.mkdirSync(path.dirname(configFile), { recursive: true });
            fs.mkdirSync(tunnelDir, { recursive: true });
            fs.writeFileSync(credentialsFile, command.pending.credentialsJson, "utf-8");
            if (process.platform !== "win32") {
                try {
                    fs.chmodSync(credentialsFile, 0o600);
                }
                catch { /* best effort */ }
            }
            if (!command.targetHostname) {
                this.report(command.operationId, "pending_failed", command.oldTunnelId, command.pending.tunnelId, "Missing target hostname");
                return;
            }
            const rules = [
                { hostname: command.targetHostname, service: `http://localhost:${this.obsPort}` },
                ...(command.targetWildcardHostname ? [{ hostname: command.targetWildcardHostname, service: `http://localhost:${this.obsPort}` }] : []),
            ];
            writeProfileTunnelConfig({
                configPath: configFile,
                tunnelId: command.pending.tunnelId,
                credentialsFile,
                rules,
            });
            const proc = spawn("cloudflared", ["tunnel", "--config", configFile, "run", command.pending.tunnelName], {
                stdio: ["ignore", "ignore", "pipe"],
                windowsHide: true,
            });
            this.pendingProcess = proc;
            this.pendingOperationId = command.operationId;
            let terminalReportSent = false;
            const stderr = [];
            proc.stderr.on("data", (data) => {
                stderr.push(data.toString());
                if (stderr.length > 5)
                    stderr.shift();
            });
            proc.once("close", (code) => {
                if (this.pendingOperationId !== command.operationId || terminalReportSent)
                    return;
                terminalReportSent = true;
                this.report(command.operationId, "pending_failed", command.oldTunnelId, command.pending?.tunnelId, `Pending cloudflared exited with code ${code}: ${stderr.join("").trim().split("\n").slice(-2).join(" | ")}`);
            });
            writeManifest({
                operationId: command.operationId,
                oldTunnelId: command.oldTunnelId,
                newTunnelId: command.pending.tunnelId,
                pendingTunnelName: command.pending.tunnelName,
                pendingCredentialsFile: credentialsFile,
                pendingConfigFile: configFile,
                pendingPid: proc.pid,
                phase: "pending_started",
                updatedAt: new Date().toISOString(),
            });
            setTimeout(() => {
                if (this.pendingOperationId !== command.operationId || terminalReportSent)
                    return;
                if (proc.exitCode !== null)
                    return;
                terminalReportSent = true;
                this.report(command.operationId, "pending_started", command.oldTunnelId, command.pending?.tunnelId);
                logger.system(`Tunnel rotation pending connector started for ${command.operationId}`);
            }, 5_000).unref();
        }
        catch (err) {
            this.report(command.operationId, "pending_failed", command.oldTunnelId, command.pending.tunnelId, err instanceof Error ? err.message : String(err));
        }
    }
    handleFinalize(command) {
        const manifest = readManifest();
        if (manifest?.operationId !== command.operationId) {
            this.report(command.operationId, "finalize_failed", command.oldTunnelId, command.newTunnelId, "Missing matching rotation manifest for finalize");
            return;
        }
        if (!manifest.pendingConfigFile || !manifest.pendingCredentialsFile || !manifest.newTunnelId) {
            this.report(command.operationId, "finalize_failed", command.oldTunnelId, command.newTunnelId, "Incomplete rotation manifest for finalize");
            return;
        }
        try {
            fs.copyFileSync(manifest.pendingConfigFile, getProfileTunnelConfigPath());
            if (process.platform !== "win32") {
                try {
                    fs.chmodSync(manifest.pendingCredentialsFile, 0o600);
                }
                catch { /* best effort */ }
            }
            stopPid(manifest.pendingPid);
            void this.stopPendingProcess();
            if (this.restartTunnel) {
                void this.restartTunnel().catch((err) => {
                    logger.warn(`Tunnel restart after rotation finalize failed: ${err instanceof Error ? err.message : String(err)}`);
                });
            }
            this.report(command.operationId, "finalize_applied", manifest.newTunnelId, undefined);
            writeManifest({
                ...manifest,
                phase: "finalize_applied",
                updatedAt: new Date().toISOString(),
            });
            logger.system(`Tunnel rotation finalized locally for ${command.operationId}`);
        }
        catch (err) {
            this.report(command.operationId, "finalize_failed", command.oldTunnelId, command.newTunnelId, err instanceof Error ? err.message : String(err));
            writeManifest({
                ...manifest,
                phase: "finalize_failed",
                updatedAt: new Date().toISOString(),
            });
        }
    }
    handleRollback(command) {
        void this.stopPendingProcess();
        const manifest = readManifest();
        stopPid(manifest?.pendingPid);
        this.report(command.operationId, "rollback_applied", command.oldTunnelId, command.newTunnelId);
        writeManifest({
            operationId: command.operationId,
            oldTunnelId: command.oldTunnelId,
            newTunnelId: command.newTunnelId,
            phase: "rollback_applied",
            updatedAt: new Date().toISOString(),
        });
        logger.system(`Tunnel rotation rollback acknowledged for ${command.operationId}`);
    }
    handleCleanup(command) {
        const manifest = readManifest();
        if (manifest?.operationId === command.operationId) {
            stopPid(manifest.pendingPid);
            removeManifest();
        }
        this.report(command.operationId, "cleanup_applied", command.newTunnelId ?? command.oldTunnelId, undefined);
        logger.system(`Tunnel rotation cleanup acknowledged for ${command.operationId}`);
    }
    report(operationId, phase, currentTunnelId, pendingTunnelId, error) {
        this.status = {
            operationId,
            phase,
            currentTunnelId,
            pendingTunnelId,
            reportedAt: new Date().toISOString(),
            ...(error ? { error } : {}),
        };
    }
    async stopPendingProcess() {
        const proc = this.pendingProcess;
        this.pendingProcess = undefined;
        this.pendingOperationId = undefined;
        if (proc?.exitCode !== null)
            return;
        await new Promise((resolve) => {
            proc.once("close", () => { resolve(); });
            try {
                proc.kill("SIGTERM");
            }
            catch {
                resolve();
            }
            setTimeout(() => {
                try {
                    proc.kill("SIGKILL");
                }
                catch { /* already gone */ }
            }, 3_000).unref();
        });
    }
}
//# sourceMappingURL=tunnel-rotation-manager.js.map