"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.processDockerAuthentication = exports.stopDockerComposeStack = exports.determineComposeFlags = exports.startDockerComposeStack = exports.deleteDockerComposeFile = exports.generateContainersDockerComposeFile = exports.DockerAuthenticationError = exports.CannotUseBothImageAndBuildContextError = exports.MissingImageOrBuildContextError = exports.UnableToParseDockerComposeFileError = exports.DockerUnableToStartError = exports.DockerUnableToPullProxySidecarImage = exports.InvalidContainerServicePort = exports.STARTUP_TIMER_BUFFER_MS = exports.STARTUP_TIMER_MS = exports.K8S_AUTH_TOKEN_FILENAME = exports.CONTAINER_SERVICE_ASSETS = exports.PROXY_SIDECAR_VOLUME_DIR = void 0;
const tslib_1 = require("tslib");
const fs = tslib_1.__importStar(require("fs"));
const path = tslib_1.__importStar(require("path"));
const yaml = tslib_1.__importStar(require("yaml"));
const http = tslib_1.__importStar(require("http"));
const docker_compose_1 = require("docker-compose");
const shared_1 = require("../shared");
const text_1 = require("../ui/text");
const tunnel_options_1 = require("./tunnel-options");
const HIDDEN_DIR = '.services';
exports.PROXY_SIDECAR_VOLUME_DIR = '/forge/container';
exports.CONTAINER_SERVICE_ASSETS = '.container-service-assets';
exports.K8S_AUTH_TOKEN_FILENAME = 'local-account';
const PROXY_SIDECAR_SERVICE_NAME = 'proxy-sidecar';
const LIFECYCLE_PATTERNS = [
    /Container .* (Starting|Started|Stopping|Stopped|Creating|Created|Recreated)/,
    /Network .* (Creating|Created|Removing|Removed)/,
    /Volume .* (Creating|Created|Removing|Removed)/,
    /\[[\+\-]\] Running/
];
exports.STARTUP_TIMER_MS = 30 * 1000;
exports.STARTUP_TIMER_BUFFER_MS = 5 * 1000;
class InvalidContainerServicePort extends shared_1.UserError {
    constructor(serviceKey) {
        super(text_1.Text.error.invalidServicePort(serviceKey));
    }
}
exports.InvalidContainerServicePort = InvalidContainerServicePort;
class DockerUnableToPullProxySidecarImage extends shared_1.UserError {
    constructor(err) {
        super(text_1.Text.tunnel.unableToPullProxySidecarImage(err?.message ?? 'Unknown Error Occurred.'));
    }
}
exports.DockerUnableToPullProxySidecarImage = DockerUnableToPullProxySidecarImage;
class DockerUnableToStartError extends shared_1.UserError {
    constructor(err) {
        super(text_1.Text.tunnel.unableToStartDockerComposeStack(err?.message ?? 'Unknown Error Occurred.'));
    }
}
exports.DockerUnableToStartError = DockerUnableToStartError;
class UnableToParseDockerComposeFileError extends shared_1.UserError {
    constructor(serviceKey, err) {
        super(text_1.Text.tunnel.UnableToParseDockerComposeFileError(serviceKey, err?.message ?? 'Unknown Error Occurred.'));
    }
}
exports.UnableToParseDockerComposeFileError = UnableToParseDockerComposeFileError;
class MissingImageOrBuildContextError extends shared_1.UserError {
    constructor(containerKey) {
        super(text_1.Text.tunnel.missingImageOrBuildContext(containerKey));
    }
}
exports.MissingImageOrBuildContextError = MissingImageOrBuildContextError;
class CannotUseBothImageAndBuildContextError extends shared_1.UserError {
    constructor(containerKey) {
        super(text_1.Text.tunnel.cannotUseBothImageAndBuildContext(containerKey));
    }
}
exports.CannotUseBothImageAndBuildContextError = CannotUseBothImageAndBuildContextError;
class DockerAuthenticationError extends shared_1.UserError {
    constructor(errCode) {
        super(text_1.Text.tunnel.unableToAuthenticateDocker(errCode));
    }
}
exports.DockerAuthenticationError = DockerAuthenticationError;
const generateContainersDockerComposeFile = async (services, appId, envId) => {
    const filesGenerated = {};
    for (const [serviceIndex, service] of services.entries()) {
        const { key: serviceKey, containers } = service;
        const containersWithTunnelConfig = containers.filter((container) => !!container.tunnel);
        if (containersWithTunnelConfig.length > 0) {
            const port = await (0, tunnel_options_1.getServicePort)(services, serviceKey);
            if (!port || port < 1024 || port > 49152) {
                throw new InvalidContainerServicePort(serviceKey);
            }
            const containerConfig = Object.fromEntries(containersWithTunnelConfig.map((container) => {
                if (!container.tunnel?.docker?.image && !container.tunnel?.docker?.build) {
                    throw new MissingImageOrBuildContextError(container.key);
                }
                else if (container.tunnel?.docker?.image && container.tunnel?.docker?.build) {
                    throw new CannotUseBothImageAndBuildContextError(container.key);
                }
                const config = {
                    container_name: container.key,
                    ...container.tunnel?.docker,
                    depends_on: [PROXY_SIDECAR_SERVICE_NAME]
                };
                if (config.build?.context) {
                    config.build.context = adjustFilePath(config.build.context);
                }
                if (config.volumes) {
                    config.volumes = config.volumes.map((volume) => adjustFilePath(volume));
                }
                if (config.develop?.watch) {
                    config.develop.watch = config.develop.watch.map((watchConfig) => ({
                        ...watchConfig,
                        path: adjustFilePath(watchConfig.path)
                    }));
                }
                const envArray = container?.tunnel?.docker.environment ?? [];
                const filteredEnvArray = envArray.filter((envVar) => !envVar.startsWith('FORGE_EGRESS_PROXY_URL='));
                filteredEnvArray.push('FORGE_EGRESS_PROXY_URL=http://proxy-sidecar:7072');
                config.environment = filteredEnvArray;
                return [container.key, config];
            }));
            const healthEndpointPath = containersWithTunnelConfig[0].health.route.path;
            const dockerComposeConfig = {
                services: {
                    ...containerConfig,
                    ...(await getProxySidecarConfig(serviceKey, Object.keys(containerConfig), port, appId, envId, serviceIndex, healthEndpointPath))
                }
            };
            const yamlString = yaml.stringify(dockerComposeConfig);
            const filePath = getContainerDockerComposePath(serviceKey);
            fs.writeFileSync(filePath, yamlString);
            filesGenerated[serviceKey] = filePath;
        }
    }
    return filesGenerated;
};
exports.generateContainersDockerComposeFile = generateContainersDockerComposeFile;
const adjustFilePath = (filePath) => {
    if (path.isAbsolute(filePath)) {
        return filePath;
    }
    let adjusted = filePath;
    if (adjusted.startsWith('./')) {
        adjusted = adjusted.slice(2);
    }
    return path.join('..', adjusted);
};
const getProxySidecarConfig = async (serviceKey, containerKeys, port, appId, envId, serviceIndex, healthEndpointPath) => {
    let fopBaseUrl = 'https://forge-outbound-proxy.services.atlassian.com';
    let jwksUrl = 'https://forge.cdn.prod.atlassian-dev.net/.well-known/jwks.json';
    let proxySidecarImage = 'forge-ecr.services.atlassian.com/forge-platform/proxy-sidecar:latest';
    if (process.env.FORGE_GRAPHQL_GATEWAY?.startsWith('https://api-private.stg.atlassian.com/graphql')) {
        fopBaseUrl = 'https://forge-outbound-proxy.stg.services.atlassian.com';
        jwksUrl = 'https://forge.cdn.stg.atlassian-dev.net/.well-known/jwks.json';
        proxySidecarImage = 'forge-ecr.stg.services.atlassian.com/forge-platform/proxy-sidecar:latest';
    }
    let appIdShort = appId;
    if (appId.startsWith('ari:cloud:ecosystem::app/')) {
        appIdShort = appId.split('/')[1];
    }
    return {
        [PROXY_SIDECAR_SERVICE_NAME]: {
            image: proxySidecarImage,
            container_name: getProxySidecarContainerName(serviceKey),
            environment: [
                `SERVICE_URL=http://${containerKeys[0]}:8080`,
                `FOP_BASE_URL=${fopBaseUrl}`,
                `APP_ID=ari:cloud:ecosystem::app/${appIdShort}`,
                `ENV_ID=ari:cloud:ecosystem::environment/${appIdShort}/${envId}`,
                `JWKS_URL=${jwksUrl}`,
                `IS_LOCAL_DEV=true`,
                `K8S_AUTH_TOKEN_PATH=${exports.PROXY_SIDECAR_VOLUME_DIR}/${exports.K8S_AUTH_TOKEN_FILENAME}`,
                `${containerKeys[0]}_CONTAINER_HEALTHCHECK=http://${containerKeys[0]}:8080${healthEndpointPath}`
            ],
            volumes: [`../${exports.CONTAINER_SERVICE_ASSETS}:${exports.PROXY_SIDECAR_VOLUME_DIR}:ro`],
            ports: [
                `${port}:${tunnel_options_1.DEFAULT_PROXY_INGRESS_PORT}`,
                `${tunnel_options_1.DEFAULT_PROXY_HEALTHCHECK_PORT_HOST_MACHINE + serviceIndex}:${tunnel_options_1.DEFAULT_PROXY_HEALTHCHECK_PORT}`
            ]
        }
    };
};
const getProxySidecarContainerName = (serviceKey) => {
    return `${PROXY_SIDECAR_SERVICE_NAME}-${serviceKey}`;
};
const getContainerDockerComposePath = (serviceKey) => {
    const hiddenDir = path.join(process.cwd(), HIDDEN_DIR);
    if (!fs.existsSync(hiddenDir)) {
        fs.mkdirSync(hiddenDir, { recursive: true });
    }
    return path.join(hiddenDir, `docker-compose-${serviceKey}.yml`);
};
const deleteDockerComposeFile = async (composeFile) => {
    if (fs.existsSync(composeFile)) {
        fs.unlinkSync(composeFile);
    }
    const hiddenDir = path.join(process.cwd(), HIDDEN_DIR);
    if (fs.existsSync(hiddenDir)) {
        const files = fs.readdirSync(hiddenDir);
        if (files.length === 0) {
            fs.rmdirSync(hiddenDir);
        }
    }
};
exports.deleteDockerComposeFile = deleteDockerComposeFile;
const startDockerComposeStack = async (dockerComposeFilePath, serviceKey, logger) => {
    try {
        await (0, docker_compose_1.pullOne)('proxy-sidecar', {
            cwd: path.dirname(dockerComposeFilePath),
            log: true,
            config: dockerComposeFilePath
        });
    }
    catch (err) {
        throw new DockerUnableToPullProxySidecarImage(err);
    }
    await waitForContainersToStart(dockerComposeFilePath, serviceKey, logger);
};
exports.startDockerComposeStack = startDockerComposeStack;
const determineComposeFlags = async (dockerComposeFilePath, serviceKey, logger) => {
    const flags = ['--build', '--quiet-pull'];
    try {
        const composeConfig = getComposeConfig(dockerComposeFilePath);
        const hasWatchConfig = Object.values(composeConfig?.services ?? {}).some((service) => service?.develop?.watch);
        if (hasWatchConfig) {
            logger.info(`Hot reload config detected. Starting up ${serviceKey} containers using the --watch flag.`);
            flags.push('--watch');
        }
    }
    catch (_) { }
    return flags;
};
exports.determineComposeFlags = determineComposeFlags;
const getComposeConfig = (dockerComposeFilePath) => {
    try {
        const composeContent = fs.readFileSync(dockerComposeFilePath, 'utf8');
        const composeConfig = yaml.parse(composeContent);
        return composeConfig;
    }
    catch (_) { }
    return undefined;
};
const extractProxySidecarHealthcheckUrl = (composeConfig) => {
    if (!composeConfig?.services) {
        return undefined;
    }
    let healthCheckHostPort = undefined;
    try {
        const proxySidecarService = composeConfig.services[PROXY_SIDECAR_SERVICE_NAME];
        if (proxySidecarService && proxySidecarService.ports && proxySidecarService.ports.length > 0) {
            const portMapping = proxySidecarService.ports.find((portMapping) => {
                const parts = portMapping.split(':');
                return parts[1] === tunnel_options_1.DEFAULT_PROXY_HEALTHCHECK_PORT.toString();
            });
            if (portMapping) {
                healthCheckHostPort = portMapping.split(':')[0];
            }
        }
    }
    catch (_) { }
    if (healthCheckHostPort) {
        return `http://localhost:${healthCheckHostPort}/health`;
    }
    return undefined;
};
const checkContainerHealth = async (url) => {
    return new Promise((resolve) => {
        const request = http.get(url, { timeout: 3000 }, (res) => {
            resolve((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300);
        });
        request.on('error', () => {
            resolve(false);
        });
        request.on('timeout', () => {
            request.destroy();
            resolve(false);
        });
    });
};
const pollContainerHealth = async (proxySidecarHealthEndpointUrl, logger) => {
    const maxAttempts = exports.STARTUP_TIMER_MS / 1000 - exports.STARTUP_TIMER_BUFFER_MS / 1000;
    const pollInterval = 1000;
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        const isHealthy = await checkContainerHealth(proxySidecarHealthEndpointUrl);
        if (isHealthy) {
            logger.info('All health endpoints responded successfully!');
            return true;
        }
        if (attempt === maxAttempts) {
            logger.warn(`Containers did not become healthy within ${exports.STARTUP_TIMER_MS / 1000}s`);
            return false;
        }
        await new Promise((resolve) => setTimeout(resolve, pollInterval));
    }
    return false;
};
const waitForContainersToStart = async (dockerComposeFilePath, serviceKey, logger) => {
    const composeFlags = await (0, exports.determineComposeFlags)(dockerComposeFilePath, serviceKey, logger);
    let proxySidecarHealthEndpointUrl;
    try {
        const composeConfig = getComposeConfig(dockerComposeFilePath);
        proxySidecarHealthEndpointUrl = extractProxySidecarHealthcheckUrl(composeConfig);
    }
    catch (err) {
        throw new UnableToParseDockerComposeFileError(serviceKey, err);
    }
    return new Promise((resolve, reject) => {
        let containersStarted = false;
        const startupTimer = setTimeout(() => {
            if (!containersStarted) {
                containersStarted = true;
                logger.info(`Startup timeout reached (${exports.STARTUP_TIMER_MS / 1000}s). Starting up the forge tunnel...`);
                resolve();
            }
        }, exports.STARTUP_TIMER_MS);
        const logFilter = (chunk) => {
            const output = chunk.toString();
            const lines = output.split('\n');
            lines.forEach((line) => {
                if (!line.trim())
                    return;
                if (LIFECYCLE_PATTERNS.some((pattern) => pattern.test(line))) {
                    logger.info(line);
                }
            });
        };
        (0, docker_compose_1.execCompose)('up', composeFlags, {
            cwd: path.dirname(dockerComposeFilePath),
            log: false,
            config: dockerComposeFilePath,
            composeOptions: [`-p${serviceKey}`],
            callback: logFilter
        }).catch((error) => {
            clearTimeout(startupTimer);
            const errorMessage = error?.err || error?.out || error?.message || 'Unknown Error Occurred.';
            reject(new DockerUnableToStartError(new Error(errorMessage)));
        });
        setTimeout(async () => {
            if (containersStarted)
                return;
            try {
                if (proxySidecarHealthEndpointUrl) {
                    logger.info(`Polling health endpoints for service: ${serviceKey}`);
                    const allHealthy = await pollContainerHealth(proxySidecarHealthEndpointUrl, logger);
                    if (allHealthy && !containersStarted) {
                        containersStarted = true;
                        clearTimeout(startupTimer);
                        resolve();
                    }
                }
                else {
                    logger.info('No health check endpoints found. Continuing with tunnel startup...');
                    containersStarted = true;
                    clearTimeout(startupTimer);
                    resolve();
                }
            }
            catch (err) {
                logger.warn(`Health check polling failed: ${err.message}. Relying on startup timeout...`);
            }
        }, exports.STARTUP_TIMER_BUFFER_MS);
    });
};
const stopDockerComposeStack = async (configFile, logger, composeFiles) => {
    if (!composeFiles || Object.keys(composeFiles).length === 0)
        return;
    const { services } = await configFile.readConfig();
    const serviceWithTunnelConfigExists = services?.some((service) => service.containers?.some((container) => {
        return !!container.tunnel;
    }));
    if (!services || services.length === 0 || !serviceWithTunnelConfigExists)
        return;
    await Promise.all(Object.entries(composeFiles).map(async ([serviceKey, file]) => {
        try {
            await (0, docker_compose_1.downAll)({
                cwd: '.',
                log: false,
                config: file,
                composeOptions: [`-p${serviceKey}`],
                callback: createCustomLogFilter(logger)
            });
            await (0, exports.deleteDockerComposeFile)(file);
        }
        catch (err) {
            throw new Error(text_1.Text.tunnel.unableToStopDockerComposeStack(serviceKey, err.message ?? 'Unknown Error Occurred.'));
        }
    }));
    deleteContainerServiceAssetsDir();
};
exports.stopDockerComposeStack = stopDockerComposeStack;
const createCustomLogFilter = (logger) => {
    let buffer = '';
    return (chunk) => {
        buffer += chunk.toString();
        const lines = buffer.split('\n');
        buffer = lines.pop() || '';
        lines.forEach((line) => {
            if (!line.trim())
                return;
            if (LIFECYCLE_PATTERNS.some((pattern) => pattern.test(line))) {
                logger.info(line);
            }
        });
    };
};
const processDockerAuthentication = async (childProcess) => {
    await new Promise((resolve, reject) => {
        childProcess.on('close', (code) => {
            if (code === 0) {
                resolve();
            }
            else {
                reject(new DockerAuthenticationError(code?.toString() || 'unknown'));
            }
        });
        childProcess.on('error', reject);
    });
};
exports.processDockerAuthentication = processDockerAuthentication;
const deleteContainerServiceAssetsDir = () => {
    const dirPath = path.join(process.cwd(), exports.CONTAINER_SERVICE_ASSETS);
    if (fs.existsSync(dirPath)) {
        const tokenFilePath = path.join(dirPath, exports.K8S_AUTH_TOKEN_FILENAME);
        if (fs.existsSync(tokenFilePath)) {
            fs.unlinkSync(tokenFilePath);
        }
        const files = fs.readdirSync(dirPath);
        if (files.length === 0) {
            fs.rmdirSync(dirPath);
        }
    }
};
