import tunnelSsh from 'tunnel-ssh';
import AppConfig from "./appConfig.js";
import BuildHostDestroyer from "./buildHost/buildHostDestroyer.js";
import BuildHostProvisioner from "./buildHost/buildHostProvisioner.js";
import BuildHostRunstateChanger from './buildHost/buildHostRunstateChanger.js';
import BuildHostWaiter from "./buildHost/buildHostWaiter.js";
import CloudClient from "./buildHost/cloudClient.js";
import constants from './constants.js';
import OutputManager from './outputManager.js';
export default class BuildHost {
    static _BUILD_HOST_APP_CONFIG_SCOPE_NAME = 'buildHost';
    static _CLOUD_APP_CONFIG_SCOPE_NAME = 'cloud';
    static _PRIVATE_KEY_FILE_NAME = constants.BUILD_HOST_PRIVATE_KEY_FILE_NAME;
    static instance;
    _DOCKER_LOCAL_HOST = 'localhost';
    _DOCKER_REMOTE_HOST = 'localhost';
    _DOCKER_REMOTE_PORT = constants.BUILD_HOST_DOCKER_REMOTE_PORT;
    _SSH_USERNAME = constants.BUILD_HOST_REMOTE_USER;
    cloudApiToken;
    cloudClient;
    cloudUsername;
    dockerLocalPort;
    environmentId;
    privateKey;
    publicKey;
    sshHost;
    sshPort;
    tunnel;
    constructor({ cloudApiToken, cloudUsername, environmentId, privateKey, publicKey, sshHost, sshPort }) {
        this.environmentId = environmentId;
        this.publicKey = publicKey;
        this.privateKey = privateKey;
        this.sshHost = sshHost;
        this.sshPort = sshPort;
        this.cloudUsername = cloudUsername;
        this.cloudApiToken = cloudApiToken;
    }
    static async get() {
        if (this.instance) {
            return this.instance;
        }
        const appConfig = await AppConfig.getInstance();
        const cloudUsername = appConfig.getConfig("username", this._CLOUD_APP_CONFIG_SCOPE_NAME);
        const cloudApiToken = appConfig.getConfig("apiToken", this._CLOUD_APP_CONFIG_SCOPE_NAME);
        const environmentId = appConfig.getConfig("environmentId", this._BUILD_HOST_APP_CONFIG_SCOPE_NAME);
        const publicKey = appConfig.getConfig("publicKey", this._BUILD_HOST_APP_CONFIG_SCOPE_NAME);
        const sshHost = appConfig.getConfig("sshHost", this._BUILD_HOST_APP_CONFIG_SCOPE_NAME);
        const sshPort = appConfig.getConfig("sshPort", this._BUILD_HOST_APP_CONFIG_SCOPE_NAME);
        const privateKey = appConfig.readConfigFile(this._PRIVATE_KEY_FILE_NAME);
        this.instance = new BuildHost({ cloudApiToken, cloudUsername, environmentId, privateKey, publicKey, sshHost, sshPort });
        return this.instance;
    }
    async createTunnel() {
        if (this.isProvisioned()) {
            const sshOptions = this.getSshConfig();
            const forwardOptions = this.getForwardConfig();
            const tunnelOptions = {
                autoClose: false,
                reconnectOnError: true
            };
            this.tunnel = (await tunnelSsh.createTunnel(tunnelOptions, {}, sshOptions, forwardOptions))[0];
            this.dockerLocalPort = this.tunnel.address().port;
            OutputManager.detail(`Established tunnel: ${this._DOCKER_LOCAL_HOST}:${this.dockerLocalPort} → ${sshOptions.host}:${sshOptions.port} → ${forwardOptions.dstAddr}:${forwardOptions.dstPort}`);
        }
        else {
            throw new Error("There is no Build Host to use.");
        }
    }
    async destroy(force = false) {
        if (!this.environmentId) {
            throw new Error("There is no Build Host to delete.");
        }
        try {
            await new BuildHostDestroyer(this.environmentId, this.getCloudClient()).destroy();
        }
        catch (error) {
            if (!force) {
                const hint = `\nIf this Build Host was already deleted externally, use \`${constants.APP_NAME} build-host destroy --force\` to reset local state.`;
                const message = error instanceof Error ? error.message : String(error);
                throw new Error(message + hint);
            }
        }
        this.publicKey = undefined;
        this.privateKey = undefined;
        this.environmentId = undefined;
        this.sshHost = undefined;
        this.sshPort = undefined;
        await this.flushToConfig();
    }
    getDockerLocalHost() {
        return this._DOCKER_LOCAL_HOST;
    }
    getDockerLocalPort() {
        return this.dockerLocalPort;
    }
    getEnvironmentId() {
        return this.environmentId;
    }
    getForwardConfig() {
        return {
            dstAddr: this._DOCKER_REMOTE_HOST,
            dstPort: this._DOCKER_REMOTE_PORT
        };
    }
    getSshConfig() {
        return {
            host: this.sshHost,
            port: this.sshPort,
            privateKey: this.privateKey,
            username: this._SSH_USERNAME
        };
    }
    getTunnel() {
        return this.tunnel;
    }
    isProvisioned() {
        return Boolean(this.environmentId);
    }
    async provision(region) {
        if (this.environmentId) {
            throw new Error(`A Build Host has already been provisioned.`);
        }
        else {
            const result = await new BuildHostProvisioner(this.getCloudClient()).provision(region);
            this.environmentId = result.environmentId;
            this.publicKey = result.publicKey;
            this.privateKey = result.privateKey;
            this.sshHost = result.sshHost;
            this.sshPort = result.sshPort;
            await this.flushToConfig();
        }
    }
    async start() {
        if (this.environmentId && this.privateKey && this.sshHost && this.sshPort) {
            await new BuildHostRunstateChanger(this.environmentId, this.getCloudClient()).start();
            await new BuildHostWaiter({
                dockerRemoteHost: this._DOCKER_REMOTE_HOST,
                dockerRemotePort: this._DOCKER_REMOTE_PORT,
                sshHost: this.sshHost,
                sshPort: this.sshPort,
                sshPrivateKey: this.privateKey,
                sshUsername: this._SSH_USERNAME,
            }).wait();
        }
        else {
            throw new Error("There is no Build Host to start.");
        }
    }
    async stop() {
        if (this.environmentId) {
            await new BuildHostRunstateChanger(this.environmentId, this.getCloudClient()).stop();
        }
        else {
            throw new Error("There is no Build Host to stop.");
        }
    }
    async suspend() {
        if (this.environmentId) {
            await new BuildHostRunstateChanger(this.environmentId, this.getCloudClient()).suspend();
        }
        else {
            throw new Error("There is no Build Host to suspend.");
        }
    }
    async flushToConfig() {
        const appConfig = await AppConfig.getInstance();
        appConfig.setConfig('environmentId', this.environmentId, BuildHost._BUILD_HOST_APP_CONFIG_SCOPE_NAME);
        appConfig.setConfig('publicKey', this.publicKey, BuildHost._BUILD_HOST_APP_CONFIG_SCOPE_NAME);
        appConfig.setConfig('sshHost', this.sshHost, BuildHost._BUILD_HOST_APP_CONFIG_SCOPE_NAME);
        appConfig.setConfig('sshPort', this.sshPort, BuildHost._BUILD_HOST_APP_CONFIG_SCOPE_NAME);
        appConfig.writeConfigFile(BuildHost._PRIVATE_KEY_FILE_NAME, this.privateKey, 0o600);
    }
    getCloudClient() {
        if (this.cloudClient) {
            return this.cloudClient;
        }
        if (this.cloudUsername && this.cloudApiToken) {
            return new CloudClient(this.cloudUsername, this.cloudApiToken);
        }
        throw new Error(`This operation requires you to log into Kyndryl Cloud Uplift first. Please try \`${constants.APP_NAME} cloud login --help\`.`);
    }
}
