"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RelayContainer = exports.DEFAULT_IMAGE_TAG = void 0;
const tslib_1 = require("tslib");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const reqp = require("request-promise");
const os = require("os");
const fs = require("fs");
const path = require("path");
const child_process_1 = require("child_process");
const inversify_1 = require("inversify");
const codiac_error_1 = require("@codiac.io/codiac-common/contracts/codiac-error");
const codiac_common_1 = require("@codiac.io/codiac-common");
const IBetterLogger_1 = require("@codiac.io/codiac-common/IBetterLogger");
const cli_tools_1 = require("@codiac.io/cli-tools");
const INTERFACES_CLI_1 = require("../ops/INTERFACES_CLI");
const Docker = require("dockerode");
const entities_1 = require("../ops/entities");
const DEFAULT_IMAGE_NAME = "codiacimages/codiac-relay";
exports.DEFAULT_IMAGE_TAG = "1.6.20";
const DEFAULT_CONTAINER_NAME = "codiac-relay";
const DEFAULT_HEALTH_CHECK_URL = "http://localhost:5799/heartbeat-json"; //  /ready-state";
const DEFAULT_RETRY_TIMOUT_IN_MINUTES = 1;
let RelayContainer = class RelayContainer {
    get userMessages() {
        return this._userMessages.asObservable();
    }
    get dockerFailed$() {
        return this.dockerFailedSubj.asObservable();
    }
    get docker$() {
        return this.dockerSubj.asObservable();
    }
    get startFailed$() {
        return this.startFailedSubj.asObservable();
    }
    get ready$() {
        return this.readySubj.pipe((0, operators_1.distinctUntilChanged)());
    }
    constructor(styler, cliEnviro, logger, healthCheckUrl = DEFAULT_HEALTH_CHECK_URL, retryTimeOut = DEFAULT_RETRY_TIMOUT_IN_MINUTES, projectRoot, localEntities = false) {
        this.styler = styler;
        this.cliEnviro = cliEnviro;
        this.logger = logger;
        this.healthCheckUrl = healthCheckUrl;
        this.retryTimeOut = retryTimeOut;
        this.projectRoot = projectRoot;
        this.localEntities = localEntities;
        this.imageName = DEFAULT_IMAGE_NAME;
        this.imageTag = exports.DEFAULT_IMAGE_TAG;
        this.containerName = DEFAULT_CONTAINER_NAME;
        this.silent = false;
        this.readyFailed$ = new rxjs_1.Subject();
        this.connectionRefusedCount = 0;
        this.serverErrorCount = 0;
        this.MAXTRIESBEFORECONTAINERRESTART = 40; // Equivalent to over 40 seconds for the container to get healthy
        this.isWin = process.platform === "win32";
        // https://github.com/apocas/dockerode/issues/290
        this.socketPath = this.isWin
            ? "//./pipe/docker_engine"
            : undefined;
        this._userMessages = new rxjs_1.BehaviorSubject(undefined);
        // private startSubj: Subject<string> = new Subject<string>();
        // private get start$(): Observable<string> {
        //   return this.startSubj.asObservable();
        // }
        this.dockerFailedSubj = new rxjs_1.Subject();
        this.dockerSubj = new rxjs_1.Subject();
        // started is based on the container we're trying to start
        this.startFailedSubj = new rxjs_1.Subject();
        this.readySubj = new rxjs_1.ReplaySubject();
        this.promisifyStream = (stream) => new Promise((resolve, reject) => {
            stream.on('data', (d) => this.logger.write(d.toString()));
            stream.on('end', resolve);
            stream.on('error', reject);
        });
        this._cliExec = new cli_tools_1.CliExec(logger);
        this.docker = new Docker();
        this.init();
    }
    /** Creates subfolders in codiac program folder required for local entities.
     *
     * Does not throw: Logs any encountered errors and continues.
    */
    createLocalDataFolders() {
        var _a;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const folders = [
                "cluster-images",
                "templates",
                "data/db",
                "data/logs"
            ];
            for (let folder of folders) {
                try {
                    const fqn = path.posix.join(this.cliEnviro.localProgramFolder, folder);
                    if (!fs.existsSync(fqn)) {
                        yield fs.promises.mkdir(fqn, { recursive: true });
                        this.logger.debug("Created local data folder ".concat("[", fqn, "]."));
                    }
                }
                catch (err) {
                    this.logger.error("Error creating local data folder ".concat("[", folder, "]: ", (_a = err.message) !== null && _a !== void 0 ? _a : "(no message available)"));
                }
            }
            return;
        });
    }
    init() {
        //Check if docker is running stream to dockerSubj or dockerFailedSubj
        this.getDocker();
        // This should hit anytime the image tag changes, which triggers the workflow to rip and replace if its not the same versions
        (0, rxjs_1.of)(this.imageTag)
            .pipe((0, operators_1.distinctUntilChanged)()) // i think to be able to change image dynamiclly
            .pipe((0, operators_1.concatMap)(_ => this.versionMismatch())) //check the version mismatch or no version do some work
            .pipe((0, operators_1.concatMap)(versionMismatch => {
            if (versionMismatch) {
                this._userMessages.next(`Version mismatch of the Relay container found. Updating to ${this.imageTag}`);
                return this.stop()
                    .pipe((0, operators_1.concatMap)(_ => this.remove()))
                    .pipe((0, operators_1.concatMap)(_ => this.deleteRelayImage()))
                    .pipe((0, operators_1.concatMap)(_ => this.pullImage()));
            }
            else {
                // The api's may already be running. Let's see.
                return (0, rxjs_1.of)("no work was done");
            }
        }))
            .subscribe(_ => this.healthCheck());
        //  If we find docker is not started, we put ourselves into a loop, asking the user to kick it on
        this.dockerFailed$
            .pipe((0, operators_1.delay)(10000))
            .pipe((0, operators_1.tap)(_ => {
            this.logger.debug("Attempting to get service again");
            this.getDocker();
        }))
            .subscribe();
        // If docker is running but the api's are not, let's try to start the container optimistically
        (0, rxjs_1.combineLatest)([this.docker$, this.readyFailed$])
            .pipe((0, operators_1.tap)(docker => {
            this.execStart();
        }))
            .pipe((0, operators_1.takeUntil)(this.ready$))
            .subscribe();
        // If docker is running but the conainer failed to start. Kick on CONTAINER from image
        (0, rxjs_1.combineLatest)([this.docker$, this.startFailed$])
            .pipe((0, operators_1.tap)(([docker, containerName]) => {
            this.createContainer();
        }))
            .pipe((0, operators_1.takeUntil)(this.ready$))
            .subscribe();
        // // Now our pipelines are defined and hot.  We fire off the catalyst and notify user.
        this.readyFailed$
            .pipe((0, operators_1.take)(1))
            .pipe((0, operators_1.tap)(() => {
            this.logger.notice(this.styler.hilite(`Starting Services...`));
        }))
            .pipe((0, operators_1.tap)(() => {
            // every three seconds we're going to health check until either ready or timeout
            (0, rxjs_1.interval)(3000)
                .pipe((0, operators_1.take)(100))
                .pipe((0, operators_1.timeout)(this.retryTimeOut * 60000)) //minutes to miliseconds
                .pipe((0, operators_1.takeUntil)(this.ready$))
                .subscribe(() => this.healthCheck());
        }))
            .subscribe();
    }
    getAllContainersOfRelayImage() {
        const command = `docker ps -a  | grep -v ID`;
        const output = new rxjs_1.Subject();
        this._cliExec.execAsyncStdOut(command, { cwd: this.projectRoot }, IBetterLogger_1.LogLevel.debug, true)
            .then((stdout) => {
            this.logger.debug(`getRelayImageFromDockerPS - entered method`);
            // Split the output into lines and parse each line as JSON
            try {
                if (stdout == undefined && stdout == "")
                    output.next(undefined);
                else {
                    const listedContainers = stdout.split("\n");
                    let containerIds = [];
                    listedContainers.forEach(r => {
                        if (r.split("   ")[1].includes(this.imageName))
                            containerIds.push(r.split("   ")[0]);
                    });
                    output.next(containerIds);
                }
            }
            catch (parseError) {
                this.logger.error(`getRelayImageFromDockerPS - Error parsing Docker output: ${parseError}`);
                return parseError;
            }
        })
            .catch(error => {
            this.logger.debug(`getRelayImageFromDockerPS - Command failed: docker ps -a  | grep -v ID`);
            output.next(undefined);
        });
        return output.asObservable();
    }
    getRelayImageFromDockerPS() {
        const command = `docker ps -a  | grep -v ID`;
        const output = new rxjs_1.Subject();
        this._cliExec.execAsyncStdOut(command, { cwd: this.projectRoot }, IBetterLogger_1.LogLevel.debug, true)
            .then((stdout) => {
            this.logger.debug(`getRelayImageFromDockerPS - entered method`);
            // Split the output into lines and parse each line as JSON
            try {
                if (stdout == undefined && stdout == "")
                    output.next(undefined);
                else {
                    const listedContainers = stdout.split("\n");
                    const relayImangeName = listedContainers.find(r => {
                        const name = r.split("   ")[1].includes(this.imageName);
                        if (name)
                            return name;
                    });
                    if (relayImangeName != undefined && relayImangeName != "") {
                        output.next(relayImangeName.split("   ")[1]);
                    }
                    else
                        output.next(undefined);
                }
            }
            catch (parseError) {
                this.logger.error(`getRelayImageFromDockerPS - Error parsing Docker output: ${parseError}`);
                return parseError;
            }
        })
            .catch(error => {
            this.logger.debug(`getRelayImageFromDockerPS - Command failed: docker ps -a  | grep -v ID`);
            output.next(undefined);
        });
        return output.asObservable();
    }
    getRunningContainers() {
        const output = new rxjs_1.Subject();
        return this.getRelayImageFromDockerPS()
            .pipe((0, operators_1.concatMap)(running => {
            if (running) {
                const command = `docker inspect ${running}`;
                this._cliExec.execAsyncStdOut(command, { cwd: this.projectRoot }, IBetterLogger_1.LogLevel.debug, true)
                    .then((stdout) => {
                    // Split the output into lines and parse each line as JSON
                    try {
                        if (stdout != undefined && stdout != "") {
                            const images = JSON.parse(stdout);
                            output.next(images);
                        }
                        else
                            output.next(undefined);
                    }
                    catch (parseError) {
                        this.logger.error(`Error getting running containers: ${parseError}`);
                        output.next(undefined);
                    }
                })
                    .catch(error => {
                    this.logger.debug(`getRunningContainers - ${error.message}`);
                    output.next(undefined);
                });
            }
            else {
                this.logger.debug(`getRunningContainers - No running containers found.`);
                return (0, rxjs_1.of)(undefined);
            }
            return output.asObservable();
        }));
    }
    versionMismatch() {
        // Confirm the latest version is running, otherwise rip and replace
        return this.getRunningContainers().pipe((0, operators_1.map)((response) => {
            this.logger.debug(`versionMismatch - ${response}`);
            if (response) {
                const currentVersion = response[0];
                return !(currentVersion.RepoTags[0].includes(exports.DEFAULT_IMAGE_TAG));
            }
            else
                return false;
        }));
    }
    deleteRelayImage() {
        return this.getRelayImages().pipe((0, operators_1.map)((response) => {
            const output = new rxjs_1.Subject();
            this.logger.debug("Deleting relay images no longer targeted");
            if (response) {
                const localRelayImages = response;
                localRelayImages
                    .filter(i => i.Tag != this.imageTag)
                    .forEach(image => {
                    this._cliExec.execAsyncStdOut(`docker rmi ${image.Repository}:${image.Tag}`, { cwd: this.projectRoot }, IBetterLogger_1.LogLevel.debug, true)
                        .then((fullfulled) => {
                        this.logger.debug('pull response', fullfulled);
                        this.createContainer();
                        output.next('Remove Image complete');
                        this._userMessages.next(`Removing extra image(s) complete`);
                    })
                        .catch(error => {
                        this.logger.debug(`Removing Relay image failed.`);
                    });
                });
            }
            else
                output.next('Remove Image complete');
            output.subscribe();
            return output.asObservable();
        }));
    }
    getRelayImages(tag) {
        const command = `docker images --format "{{json .}}" ${this.imageName}${(tag) ? ":" + tag : ""}`;
        const output = new rxjs_1.Subject();
        this._cliExec.execAsyncStdOut(command, { cwd: this.projectRoot }, IBetterLogger_1.LogLevel.debug, true)
            .then((stdout) => {
            // Split the output into lines and parse each line as JSON
            try {
                if (stdout != undefined && stdout != "") {
                    const lines = stdout.trim().split('\n');
                    const images = lines.map(line => JSON.parse(line));
                    output.next(images);
                }
                else
                    output.next(undefined);
            }
            catch (parseError) {
                this.logger.error(`Error getting relay image: ${parseError}`);
                return parseError;
            }
        })
            .catch(error => {
            this.logger.debug(`get relay images failed.`);
        });
        return output.asObservable();
    }
    dockerSetRelayRestart() {
        const command = `docker update --restart unless-stopped ${this.containerName}`;
        const output = new rxjs_1.Subject();
        this._cliExec.execAsyncStdOut(command, { cwd: this.projectRoot }, IBetterLogger_1.LogLevel.debug, true)
            .then((stdout) => {
            // Split the output into lines and parse each line as JSON
            try {
                output.next(undefined);
            }
            catch (parseError) {
                return parseError;
            }
        })
            .catch(error => {
            this.logger.debug(`dockerSetRelayRestart - get running containers Failed.`);
            output.next(undefined);
        });
        return output.asObservable();
    }
    healthCheck() {
        var _a, _b;
        try {
            this.logger.debug(`healthCheck - Checking ${this.containerName} health now...`);
            reqp.get(this.healthCheckUrl)
                .then(response => {
                this.logger.trace(`healthCheck - ...${this.containerName} ready.`);
                // this.watchLogs();
                this.readySubj.next(true);
            })
                .catch(error => {
                var _a, _b;
                // Sometimes the relay container hangs and the heartbeat is never reachable.  
                // Since we're pinging it every second, let's simply give up and restart the container after bunch of tries
                if (error.error.code == "ECONNREFUSED") {
                    this.connectionRefusedCount = this.connectionRefusedCount + 1;
                    if (this.connectionRefusedCount > this.MAXTRIESBEFORECONTAINERRESTART) {
                        this.createContainer().pipe((0, operators_1.take)(1)).subscribe();
                        this.connectionRefusedCount = 0;
                    }
                }
                this.logger.debug(`healthCheck - ...${this.containerName} failed to respond (${(_a = error.name) !== null && _a !== void 0 ? _a : ""} ${(_b = error.message) !== null && _b !== void 0 ? _b : ""})`);
                this.readyFailed$.next();
                // this.startSubj.next(this.containerName);
            });
        }
        catch (error) {
            this.logger.debug(`healthCheck failed (${(_a = error.name) !== null && _a !== void 0 ? _a : ""} ${(_b = error.message) !== null && _b !== void 0 ? _b : ""})`, JSON.stringify(error, null, 2));
            this.readyFailed$.next();
        }
    }
    stop() {
        // fire off this command in a separate cli window
        let cmdStop = `docker stop ${this.containerName}`;
        let result$ = new rxjs_1.Subject();
        result$.subscribe();
        let options = {
            cwd: this.projectRoot,
            // stdio: "inherit",
            // stdio: [process.stdin, process.stdout, process.stderr, process.st],  // TODO:  Capture stderr instead
            shell: true
        };
        const ls = (0, child_process_1.spawn)(cmdStop, options);
        // ls.stdout?.on('data', (data: any) => {
        //   result$.next(true);
        // });
        // ls.stdout?.on('error', (data: any) => {
        //   result$.next(false);
        // });
        // ls.on('error', (data: any) => {
        //   result$.next(false);
        // });
        ls.on('close', (data) => {
            result$.next(false);
        });
        return result$.asObservable();
    }
    remove() {
        // fire off this command in a separate cli window
        this.getAllContainersOfRelayImage().pipe((0, operators_1.map)(response => {
            if (response && response.length > 0) {
                response.forEach(containerId => {
                    let cmdStop = `docker rm ${containerId} -f`;
                    let options = {
                        cwd: this.projectRoot,
                        shell: true
                    };
                    const ls = (0, child_process_1.spawn)(cmdStop, options);
                    ls.on('close', (data) => {
                        result$.next(false);
                    });
                });
            }
        }))
            .subscribe();
        let result$ = new rxjs_1.Subject();
        result$.subscribe();
        return result$.asObservable();
    }
    destroy() {
        throw new codiac_error_1.CodiacError("Not yet implemented");
    }
    createContainer() {
        var _a, _b;
        this.logger.debug(`createContainer - Creating ${this.containerName} from image: ${this.imageName}:${this.imageTag}`);
        let container = {
            Image: `${this.imageName}:${this.imageTag}`,
            name: `${this.containerName}`,
            Hostname: `${this.containerName}`,
            ExposedPorts: {
                "5799/tcp": {},
                "5798/tcp": {},
                "4222/tcp": {},
                "9222/tcp": {}
            },
            HostConfig: {
                Binds: [
                    `${os.homedir}/.codiac:/root/.codiac`
                    // `${os.homedir}/.kube:/root/.kube`,
                    // `${os.homedir}/.azure:/root/.azure`
                ],
                PortBindings: {
                    "5799/tcp": [
                        {
                            HostPort: "5799"
                        }
                    ],
                    "5798/tcp": [
                        {
                            HostPort: "5798"
                        }
                    ],
                    "4222/tcp": [
                        {
                            HostPort: "4222"
                        }
                    ],
                    "9222/tcp": [
                        {
                            HostPort: "9222"
                        }
                    ]
                },
                RestartPolicy: {
                    Name: "unless-stopped"
                }
            },
            Volumes: {
                "/root/.codiac": {}
                // "/root/.kube": {},
                // "/root/.azure": {}
            },
            Env: [
                "HOST_PLATFORM=".concat(process.platform)
            ]
        };
        if (this.localEntities == true) {
            container.ExposedPorts["27017/tcp"] = {};
            container.HostConfig.PortBindings["27017/tcp"] = [{ HostPort: "27017" }];
            (_a = container.HostConfig.Binds) === null || _a === void 0 ? void 0 : _a.push(`${os.homedir}/.codiac/data/db:/data/db`);
            (_b = container.HostConfig.Binds) === null || _b === void 0 ? void 0 : _b.push(`${os.homedir}/.codiac/data/logs:/data/logs`);
            container.Volumes["/data/db"] = {};
            container.Volumes["/data/logs"] = {};
            container.Entrypoint = ["./start-relay-w-nats-and-local-entities.sh"];
        }
        const output = new rxjs_1.Subject();
        this.docker.createContainer(container)
            .then(container => {
            output.next(container);
            this.logger.trace(`createContainer - Created ${this.containerName}`);
            this.execStart();
        })
            .catch(error => {
            if (error.statusCode == 409)
                this.execStart(); //Already in use
            else {
                this.logger.debug(`createContainer - Failed to create ${this.containerName}`, JSON.stringify(error, null, 2));
                this.pullImage();
            }
            output.next(error);
        });
        output.subscribe();
        return output.asObservable();
    }
    pullImage() {
        const output = new rxjs_1.Subject();
        this.logger.notice("Fetching Codiac Relay image. This may take a few minutes...");
        this._userMessages.next(`Fetching Codiac Relay image. This may take a few minutes...`);
        this._cliExec.execAsyncStdOut(`docker pull ${this.imageName}:${this.imageTag}`, { cwd: this.projectRoot }, IBetterLogger_1.LogLevel.debug, true)
            .then((fullfulled) => {
            this.logger.debug('pull response', fullfulled);
            this.createContainer();
            output.next('pull complete');
            this._userMessages.next(`Fetch complete`);
        })
            .catch(error => {
            const msg = `Fetching failed: ${error.stderr}`;
            this.logger.error(msg);
            this._userMessages.next(msg);
        });
        output.subscribe();
        return output.asObservable();
    }
    execStart() {
        (0, rxjs_1.of)(this.createLocalDataFolders()) // Run host machine configs first, then proceed to start the container.
            .pipe((0, operators_1.map)(response => {
            this.docker.getContainer(this.containerName).start().then(container => {
                this._userMessages.next(`Starting Relay...`);
                this.dockerSetRelayRestart().pipe((0, operators_1.map)(_ => this.healthCheck())).subscribe();
                this.logger.debug(`execStart - Container ${container.id} started`);
            })
                .catch(error => {
                this.logger.debug(`execStart - Failed to start ${this.containerName}`, error.message);
                if (error.statusCode == 404)
                    this.createContainer().pipe((0, operators_1.take)(1)).subscribe();
                // If it's all gone to hell and that server isnt responding. kill it and try again.
                if (error.statusCode == 500)
                    this.serverErrorCount = this.serverErrorCount + 1;
                // Hard stop on 20 tries, reboot it
                if (this.serverErrorCount == 20) {
                    this.stop()
                        .pipe((0, operators_1.map)(x => this.startFailedSubj.next(this.containerName)))
                        .pipe((0, operators_1.take)(1))
                        .subscribe();
                    this.serverErrorCount = 0;
                }
            });
        }))
            .subscribe();
    }
    watchLogs() {
        const container = this.docker.getContainer(this.containerName);
        container.logs({
            follow: true,
            stdout: true,
            stderr: true
        })
            .then(stream => {
            this.promisifyStream(stream);
        });
    }
    getDocker() {
        this.logger.debug("getDocker - initialize");
        const docker = this.socketPath ? new Docker({ socketPath: this.socketPath }) : new Docker();
        (0, rxjs_1.from)(docker.info())
            .pipe((0, operators_1.map)((info) => {
            this.logger.debug("getDocker - check docker info");
            if (!info)
                throw null;
            this.docker = docker;
            this.dockerSubj.next(docker);
            this.logger.debug("Docker is running");
            return this.docker;
        }))
            .pipe((0, operators_1.catchError)(error => {
            // cli.action.stop();
            const msg = 'Can you start docker for me?';
            this.logger.error(msg);
            this._userMessages.next(msg);
            this.dockerFailedSubj.next();
            return (0, rxjs_1.of)(this.docker);
        }))
            .pipe((0, operators_1.take)(1))
            .subscribe();
    }
};
RelayContainer = tslib_1.__decorate([
    tslib_1.__param(0, (0, inversify_1.inject)(INTERFACES_CLI_1.INTERFACES_CLI.IOutputStyler)),
    tslib_1.__param(1, (0, inversify_1.inject)(entities_1.CliEnviro)),
    tslib_1.__param(2, (0, inversify_1.inject)(codiac_common_1.INTERFACES.IBetterLogger)),
    tslib_1.__metadata("design:paramtypes", [Object, entities_1.CliEnviro, Object, String, Number, String, Boolean])
], RelayContainer);
exports.RelayContainer = RelayContainer;
//# sourceMappingURL=relay-container.js.map