import { Resource } from "../resource.js";
import { secret } from "../secret.js";
import { diff } from "../util/diff.js";
import { createClickhouseApi } from "./api.js";
import { OrganizationRef } from "./organization.js";
/**
 * Create, manage and delete Clickhouse services
 *
 * @example
 * // Create a basic Clickhouse service on aws
 * const organization = await OrganizationRef("Alchemy's Organization");
 * const service = await Service("clickhouse", {
 *   organization,
 *   provider: "aws",
 *   region: "us-east-1",
 * });
 *
 * @example
 * // Create a basic Clickhouse service on aws with custom scaling rules
 * const service = await Service("clickhouse", {
 *   organization,
 *   provider: "aws",
 *   region: "us-east-1",
 *   minReplicaMemoryGb: 8,
 *   maxReplicaMemoryGb: 356,
 *   numReplicas: 3,
 * });
 */
export const Service = Resource("clickhouse::Service", async function (id, props) {
    const api = createClickhouseApi({
        keyId: props.keyId,
        secret: props.secret,
    });
    const idleScaling = props.idleScaling ?? true;
    const isReadonly = props.isReadonly ?? false;
    const releaseChannel = props.releaseChannel ?? "default";
    const endpoints = [];
    const enableMysqlEndpoint = props.enableMysqlEndpoint ?? true;
    if (enableMysqlEndpoint) {
        endpoints.push({ protocol: "mysql", enabled: true });
    }
    const waitForHttpEndpointReady = props.waitForHttpEndpointReady ?? true;
    //todo(michael): comment these in when disabling is supported
    // const enableHttpsEndpoint = props.enableHttpsEndpoint ?? true;
    // if (enableHttpsEndpoint) {
    // 	endpoints.push({ protocol: "https", enabled: true });
    // }
    // const enableNativesecureEndpoint = props.enableNativesecureEndpoint ?? true;
    // if (enableNativesecureEndpoint) {
    // 	endpoints.push({ protocol: "nativesecure", enabled: true });
    // }
    const stateTarget = props.stateTarget ?? "start";
    const ipAccessList = props.ipAccessList ?? [
        {
            description: "Anywhere",
            source: "0.0.0.0/0",
        },
    ];
    const minReplicaMemoryGb = props.minReplicaMemoryGb ?? 8;
    const maxReplicaMemoryGb = props.maxReplicaMemoryGb ?? 356;
    const numReplicas = props.numReplicas ?? 3;
    const idleTimeoutMinutes = props.idleTimeoutMinutes ?? 15;
    const name = props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
    const organizationId = typeof props.organization === "string"
        ? await OrganizationRef(props.organization)
            .then((organization) => organization.id)
            .catch(() => props.organization)
        : props.organization.id;
    if (this.phase === "delete") {
        await api.updateServiceState({
            path: {
                organizationId: organizationId,
                serviceId: this.output.clickhouseId,
            },
            body: {
                command: "stop",
            },
        });
        await waitForServiceState(api, organizationId, this.output.clickhouseId, (state) => state === "stopped");
        await api.deleteService({
            path: {
                organizationId: organizationId,
                serviceId: this.output.clickhouseId,
            },
        });
        await waitForServiceDeletion(api, organizationId, this.output.clickhouseId);
        return this.destroy();
    }
    if (this.phase === "update") {
        const resourceDiff = diff({
            ...props,
            idleScaling,
            isReadonly,
            releaseChannel,
            name,
        }, {
            ...this.output,
            organization: props.organization,
        });
        const updates = {};
        if (resourceDiff.some((prop) => prop !== "name" &&
            prop !== "ipAccessList" &&
            prop !== "releaseChannel" &&
            prop !== "enableMysqlEndpoint" &&
            prop !== "enableHttpsEndpoint" &&
            prop !== "enableNativesecureEndpoint" &&
            prop !== "minReplicaMemoryGb" &&
            prop !== "maxReplicaMemoryGb" &&
            prop !== "numReplicas" &&
            prop !== "idleScaling" &&
            prop !== "idleTimeoutMinutes" &&
            prop !== "stateTarget")) {
            return this.replace();
        }
        if (
        //todo(michael): check encryption key swap?
        resourceDiff.some((prop) => prop === "name" ||
            prop === "ipAccessList" ||
            prop === "releaseChannel") ||
            enableMysqlEndpoint !== this.output.enableMysqlEndpoint) {
            const ipAccessListToRemove = this.output.ipAccessList.filter((ipAccessList) => !props.ipAccessList?.includes(ipAccessList));
            const ipAccessListToAdd = props.ipAccessList?.filter((ipAccessList) => !this.output.ipAccessList.includes(ipAccessList));
            const response = (await api.updateServiceBasicDetails({
                path: {
                    organizationId: organizationId,
                    serviceId: this.output.clickhouseId,
                },
                body: {
                    name,
                    ipAccessList: {
                        remove: ipAccessListToRemove,
                        add: ipAccessListToAdd,
                    },
                    releaseChannel,
                    endpoints,
                },
            })).data.result;
            updates.name = response.name;
            updates.ipAccessList = response.ipAccessList;
            updates.releaseChannel = response.releaseChannel;
            updates.mysqlEndpoint = response.endpoints.find((endpoint) => endpoint.protocol === "mysql");
            updates.httpsEndpoint = response.endpoints.find((endpoint) => endpoint.protocol === "https");
            updates.nativesecureEndpoint = response.endpoints.find((endpoint) => endpoint.protocol === "nativesecure");
        }
        if (stateTarget !== this.output.stateTarget) {
            const response = await api.updateServiceState({
                path: {
                    organizationId: organizationId,
                    serviceId: this.output.clickhouseId,
                },
                body: {
                    command: stateTarget,
                },
            });
            updates.state = response.data.result.state;
            updates.stateTarget = stateTarget;
        }
        if (resourceDiff.some((prop) => prop === "minReplicaMemoryGb" ||
            prop === "maxReplicaMemoryGb" ||
            prop === "numReplicas" ||
            prop === "idleScaling" ||
            prop === "idleTimeoutMinutes")) {
            const response = (await api.updateServiceAutoScalingSettings2({
                path: {
                    organizationId: organizationId,
                    serviceId: this.output.clickhouseId,
                },
                body: {
                    minReplicaMemoryGb: props.minReplicaMemoryGb,
                    maxReplicaMemoryGb: props.maxReplicaMemoryGb,
                    numReplicas: props.numReplicas,
                    idleScaling: props.idleScaling,
                    idleTimeoutMinutes: idleTimeoutMinutes,
                },
            })).data.result;
            updates.minReplicaMemoryGb = response.minReplicaMemoryGb;
            updates.maxReplicaMemoryGb = response.maxReplicaMemoryGb;
            updates.numReplicas = response.numReplicas;
            updates.idleScaling = response.idleScaling;
            updates.idleTimeoutMinutes = response.idleTimeoutMinutes;
        }
        return {
            ...this.output,
            ...updates,
        };
    }
    const response = (await api.createNewService({
        path: {
            organizationId: organizationId,
        },
        body: {
            name,
            provider: props.provider,
            region: props.region,
            ipAccessList: ipAccessList,
            minReplicaMemoryGb: minReplicaMemoryGb,
            maxReplicaMemoryGb: maxReplicaMemoryGb,
            numReplicas: numReplicas,
            idleScaling: idleScaling,
            idleTimeoutMinutes: idleTimeoutMinutes,
            isReadonly: isReadonly,
            dataWarehouseId: props.dataWarehouseId,
            backupId: props.backupId,
            encryptionKey: props.encryptionKey,
            encryptionAssumedRoleIdentifier: props.encryptionAssumedRoleIdentifier,
            privatePreviewTermsChecked: true,
            releaseChannel: releaseChannel,
            byocId: props.byocId,
            hasTransparentDataEncryption: props.hasTransparentDataEncryption ?? false,
            endpoints: endpoints,
            profile: props.profile,
            complianceType: props.complianceType,
        },
    })).data.result;
    const password = response.password;
    const service = response.service;
    const httpEndpoint = service.endpoints.find((endpoint) => endpoint.protocol === "https");
    await waitForServiceState(api, organizationId, response.service.id, (state) => state === "running" || state === "idle");
    if (waitForHttpEndpointReady && httpEndpoint) {
        await waitForHttpServiceReady(httpEndpoint, {
            password: password,
            username: "default",
        });
    }
    return {
        organizationId: organizationId,
        name: service.name,
        clickhouseId: service.id,
        password: secret(password),
        provider: service.provider,
        region: service.region,
        ipAccessList: service.ipAccessList,
        minReplicaMemoryGb: service.minReplicaMemoryGb,
        maxReplicaMemoryGb: service.maxReplicaMemoryGb,
        numReplicas: service.numReplicas,
        idleScaling: service.idleScaling,
        idleTimeoutMinutes: service.idleTimeoutMinutes,
        isReadonly: service.isReadonly,
        dataWarehouseId: service.dataWarehouseId,
        backupId: props.backupId,
        encryptionKey: service.encryptionKey,
        encryptionAssumedRoleIdentifier: service.encryptionAssumedRoleIdentifier,
        releaseChannel: service.releaseChannel,
        byocId: service.byocId,
        hasTransparentDataEncryption: service.hasTransparentDataEncryption,
        profile: service.profile,
        complianceType: service.complianceType,
        stateTarget,
        state: service.state,
        mysqlEndpoint: service.endpoints.find((endpoint) => endpoint.protocol === "mysql"),
        httpsEndpoint: httpEndpoint,
        nativesecureEndpoint: service.endpoints.find((endpoint) => endpoint.protocol === "nativesecure"),
    };
});
async function waitForServiceState(api, organizationId, serviceId, stateChecker) {
    async function checkState() {
        const service = await api.getServiceDetails({
            path: {
                organizationId: organizationId,
                serviceId: serviceId,
            },
        });
        const serviceState = service.data.result.state;
        if (stateChecker(serviceState)) {
            return;
        }
        throw new Error(`Service ${serviceId} is in state ${serviceState}`);
    }
    await waitFor(checkState, 10 * 60);
}
async function waitForHttpServiceReady(endpoint, credentials) {
    async function checkHttpEndpoint() {
        await fetch(`https://${endpoint.host}:${endpoint.port}/?query=SELECT%20count%28%29%20FROM%20system.databases%20WHERE%20name%20%3D%20%27default%27`, {
            headers: {
                Authorization: `Basic ${btoa(`${credentials.username}:${credentials.password}`)}`,
            },
        })
            .then((res) => res.text())
            .then((text) => {
            if (text === "0") {
                throw new Error("Service is not ready");
            }
            return;
        });
    }
    await waitFor(checkHttpEndpoint, 10 * 60);
}
async function waitForServiceDeletion(api, organizationId, serviceId) {
    async function checkDeletion() {
        const status = await api
            .getServiceDetails({
            path: {
                organizationId: organizationId,
                serviceId: serviceId,
            },
        })
            .then((service) => service.response.status)
            .catch((error) => {
            return error.status ?? 500;
        });
        if (status === 404) {
            return;
        }
        throw new Error(`Service ${serviceId} is not deleted`);
    }
    await waitFor(checkDeletion, 10 * 60);
}
async function waitFor(checkFunction, maxWaitSeconds) {
    if (maxWaitSeconds < 5) {
        maxWaitSeconds = 5;
    }
    const maxRetries = Math.floor(maxWaitSeconds / 5);
    for (let attempt = 0; attempt < maxRetries; attempt++) {
        try {
            await checkFunction();
            return;
        }
        catch (error) {
            if (attempt === maxRetries - 1) {
                throw error;
            }
            await new Promise((resolve) => setTimeout(resolve, 5000));
        }
    }
}
//# sourceMappingURL=service.js.map