import path from "path";
import url from "url";
import fs from "fs";
import { VisibleError } from "../error.js";
import { execAsync } from "../util/process.js";
import { existsAsync } from "../util/fs.js";
import { Colors } from "../cli/colors.js";
import { Construct } from "constructs";
import { Duration as CdkDuration, IgnoreMode } from "aws-cdk-lib/core";
import { Role, Effect, PolicyStatement, AccountPrincipal, ServicePrincipal, CompositePrincipal, } from "aws-cdk-lib/aws-iam";
import { ViewerProtocolPolicy, AllowedMethods, CachedMethods, CachePolicy, CacheQueryStringBehavior, CacheHeaderBehavior, CacheCookieBehavior, OriginProtocolPolicy, OriginRequestPolicy, } from "aws-cdk-lib/aws-cloudfront";
import { HttpOrigin } from "aws-cdk-lib/aws-cloudfront-origins";
import { Stack } from "./Stack.js";
import { Distribution } from "./Distribution.js";
import { Function } from "./Function.js";
import { Secret } from "./Secret.js";
import { useDeferredTasks } from "./deferred_task.js";
import { attachPermissionsToRole } from "./util/permission.js";
import { bindEnvironment, bindPermissions, getParameterPath, getReferencedSecrets, } from "./util/functionBinding.js";
import { useProject } from "../project.js";
import { Vpc, } from "aws-cdk-lib/aws-ec2";
import { AwsLogDriver, Cluster, FargateTaskDefinition, ContainerImage, FargateService, } from "aws-cdk-lib/aws-ecs";
import { LogGroup, LogRetention, RetentionDays } from "aws-cdk-lib/aws-logs";
import { Platform } from "aws-cdk-lib/aws-ecr-assets";
import { ApplicationLoadBalancer, } from "aws-cdk-lib/aws-elasticloadbalancingv2";
import { createAppContext } from "./context.js";
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
const NIXPACKS_IMAGE_NAME = "sst-nixpacks";
const supportedCpus = {
    "0.25 vCPU": 256,
    "0.5 vCPU": 512,
    "1 vCPU": 1024,
    "2 vCPU": 2048,
    "4 vCPU": 4096,
    "8 vCPU": 8192,
    "16 vCPU": 16384,
};
const supportedMemories = {
    "0.25 vCPU": {
        "0.5 GB": 512,
        "1 GB": 1024,
        "2 GB": 2048,
    },
    "0.5 vCPU": {
        "1 GB": 1024,
        "2 GB": 2048,
        "3 GB": 3072,
        "4 GB": 4096,
    },
    "1 vCPU": {
        "2 GB": 2048,
        "3 GB": 3072,
        "4 GB": 4096,
        "5 GB": 5120,
        "6 GB": 6144,
        "7 GB": 7168,
        "8 GB": 8192,
    },
    "2 vCPU": {
        "4 GB": 4096,
        "5 GB": 5120,
        "6 GB": 6144,
        "7 GB": 7168,
        "8 GB": 8192,
        "9 GB": 9216,
        "10 GB": 10240,
        "11 GB": 11264,
        "12 GB": 12288,
        "13 GB": 13312,
        "14 GB": 14336,
        "15 GB": 15360,
        "16 GB": 16384,
    },
    "4 vCPU": {
        "8 GB": 8192,
        "9 GB": 9216,
        "10 GB": 10240,
        "11 GB": 11264,
        "12 GB": 12288,
        "13 GB": 13312,
        "14 GB": 14336,
        "15 GB": 15360,
        "16 GB": 16384,
        "17 GB": 17408,
        "18 GB": 18432,
        "19 GB": 19456,
        "20 GB": 20480,
        "21 GB": 21504,
        "22 GB": 22528,
        "23 GB": 23552,
        "24 GB": 24576,
        "25 GB": 25600,
        "26 GB": 26624,
        "27 GB": 27648,
        "28 GB": 28672,
        "29 GB": 29696,
        "30 GB": 30720,
    },
    "8 vCPU": {
        "16 GB": 16384,
        "20 GB": 20480,
        "24 GB": 24576,
        "28 GB": 28672,
        "32 GB": 32768,
        "36 GB": 36864,
        "40 GB": 40960,
        "44 GB": 45056,
        "48 GB": 49152,
        "52 GB": 53248,
        "56 GB": 57344,
        "60 GB": 61440,
    },
    "16 vCPU": {
        "32 GB": 32768,
        "40 GB": 40960,
        "48 GB": 49152,
        "56 GB": 57344,
        "64 GB": 65536,
        "72 GB": 73728,
        "80 GB": 81920,
        "88 GB": 90112,
        "96 GB": 98304,
        "104 GB": 106496,
        "112 GB": 114688,
        "120 GB": 122880,
    },
};
/**
 * The `Service` construct is a higher level CDK construct that makes it easy to create modern web apps with Server Side Rendering capabilities.
 * @example
 * Deploys a service in the `app` directory.
 *
 * ```js
 * new Service(stack, "myApp", {
 *   path: "app",
 * });
 * ```
 */
export class Service extends Construct {
    id;
    props;
    doNotDeploy;
    devFunction;
    vpc;
    cluster;
    container;
    taskDefinition;
    distribution;
    constructor(scope, id, props) {
        super(scope, id);
        const app = scope.node.root;
        const stack = Stack.of(this);
        this.id = id;
        this.props = {
            path: ".",
            cpu: props?.cpu || "0.25 vCPU",
            memory: props?.memory || "0.5 GB",
            port: props?.port || 3000,
            logRetention: props?.logRetention || "infinite",
            waitForInvalidation: false,
            ...props,
        };
        this.doNotDeploy =
            !stack.isActive || (app.mode === "dev" && !this.props.dev?.deploy);
        this.validateServiceExists();
        this.validateMemoryAndCpu();
        useServices().add(stack.stackName, id, this.props);
        if (this.doNotDeploy) {
            // @ts-expect-error
            this.vpc = this.cluster = this.container = this.taskDefinition = null;
            // @ts-expect-error
            this.distribution = null;
            this.devFunction = this.createDevFunction();
            return;
        }
        // Create ECS cluster
        const vpc = this.createVpc();
        const { cluster, container, taskDefinition, service } = this.createService(vpc);
        const { alb, target } = this.createLoadBalancer(vpc, service);
        this.createAutoScaling(service, target);
        // Create Distribution
        this.distribution = this.createDistribution(alb);
        this.vpc = vpc;
        this.cluster = cluster;
        this.container = container;
        this.taskDefinition = taskDefinition;
        this.bindForService(props?.bind || []);
        this.attachPermissionsForService(props?.permissions || []);
        Object.entries(props?.environment || {}).map(([key, value]) => this.addEnvironmentForService(key, value));
        useDeferredTasks().add(async () => {
            if (!app.isRunningSSTTest()) {
                Colors.line(`➜  Building the container image for the "${this.node.id}" service...`);
                // Build app
                let dockerfile;
                if (this.props.file) {
                    dockerfile = this.props.file;
                }
                else if (await existsAsync(path.join(this.props.path, "Dockerfile"))) {
                    dockerfile = "Dockerfile";
                }
                else {
                    await this.createNixpacksBuilder();
                    dockerfile = await this.runNixpacksBuild();
                }
                await this.runDockerBuild(dockerfile);
                this.updateContainerImage(dockerfile, taskDefinition, container);
            }
            // Invalidate CloudFront
            this.distribution.createInvalidation();
        });
    }
    /////////////////////
    // Public Properties
    /////////////////////
    /**
     * The CloudFront URL of the website.
     */
    get url() {
        if (this.doNotDeploy)
            return this.props.dev?.url;
        return this.distribution.url;
    }
    /**
     * If the custom domain is enabled, this is the URL of the website with the
     * custom domain.
     */
    get customDomainUrl() {
        if (this.doNotDeploy)
            return;
        return this.distribution.customDomainUrl;
    }
    /**
     * The internally created CDK resources.
     */
    get cdk() {
        if (this.doNotDeploy)
            return;
        return {
            vpc: this.vpc,
            cluster: this.cluster,
            distribution: this.distribution.cdk.distribution,
            hostedZone: this.distribution.cdk.hostedZone,
            certificate: this.distribution.cdk.certificate,
        };
    }
    /////////////////////
    // Public Methods
    /////////////////////
    getConstructMetadata() {
        return {
            type: "Service",
            data: {
                mode: this.doNotDeploy
                    ? "placeholder"
                    : "deployed",
                path: this.props.path,
                customDomainUrl: this.customDomainUrl,
                url: this.url,
                devFunction: this.devFunction?.functionArn,
                task: this.taskDefinition?.taskDefinitionArn,
                container: this.container?.containerName,
                secrets: (this.props.bind || [])
                    .filter((c) => c instanceof Secret)
                    .map((c) => c.name),
            },
        };
    }
    /** @internal */
    getFunctionBinding() {
        const app = this.node.root;
        return {
            clientPackage: "service",
            variables: {
                url: this.doNotDeploy
                    ? {
                        type: "plain",
                        value: this.props.dev?.url ?? "localhost",
                    }
                    : {
                        // Do not set real value b/c we don't want to make the Lambda function
                        // depend on the Site. B/c often the site depends on the Api, causing
                        // a CloudFormation circular dependency if the Api and the Site belong
                        // to different stacks.
                        type: "site_url",
                        value: this.customDomainUrl || this.url,
                    },
            },
            permissions: {
                "ssm:GetParameters": [
                    `arn:${Stack.of(this).partition}:ssm:${app.region}:${app.account}:parameter${getParameterPath(this, "url")}`,
                ],
            },
        };
    }
    /**
     * Binds additional resources to service.
     *
     * @example
     * ```js
     * service.bind([STRIPE_KEY, bucket]);
     * ```
     */
    bind(constructs) {
        this.devFunction?.bind(constructs);
        this.bindForService(constructs);
    }
    /**
     * Attaches the given list of permissions to allow the service
     * to access other AWS resources.
     *
     * @example
     * ```js
     * service.attachPermissions(["sns"]);
     * ```
     */
    attachPermissions(permissions) {
        this.devFunction?.attachPermissions(permissions);
        this.attachPermissionsForService(permissions);
    }
    /**
     * Attaches additional environment variable to the service.
     *
     * @example
     * ```js
     * service.addEnvironment({
     *   DEBUG: "*"
     * });
     * ```
     */
    addEnvironment(name, value) {
        this.devFunction?.addEnvironment(name, value);
        this.addEnvironmentForService(name, value);
    }
    /////////////////////
    // Bundle Cluster
    /////////////////////
    validateServiceExists() {
        const { path: servicePath, file } = this.props;
        if (!fs.existsSync(servicePath)) {
            throw new Error(`No service found at "${path.resolve(servicePath)}"`);
        }
        if (file) {
            const dockerfilePath = path.join(servicePath, file);
            if (!fs.existsSync(dockerfilePath)) {
                throw new Error(`No Dockerfile found at "${dockerfilePath}". Make sure to set the "file" property to the path of the Dockerfile relative to "${servicePath}".`);
            }
        }
    }
    validateMemoryAndCpu() {
        const { memory, cpu } = this.props;
        if (!supportedCpus[cpu]) {
            throw new Error(`Only the following "cpu" settings are supported for the ${this.node.id} service: ${Object.keys(supportedCpus).join(", ")}`);
        }
        // @ts-ignore
        if (!supportedMemories[cpu][memory]) {
            throw new Error(`Only the following "memory" settings are supported with "${cpu}" for the ${this.node.id} service: ${Object.keys(supportedMemories[cpu]).join(", ")}`);
        }
    }
    createVpc() {
        const { cdk } = this.props;
        return (cdk?.vpc ??
            new Vpc(this, "Vpc", {
                natGateways: 1,
            }));
    }
    createService(vpc) {
        const { cpu, memory, port, logRetention, cdk } = this.props;
        const app = this.node.root;
        const clusterName = app.logicalPrefixedName(this.node.id);
        const logGroup = new LogRetention(this, "LogRetention", {
            logGroupName: `/sst/service/${clusterName}`,
            retention: RetentionDays[logRetention.toUpperCase()],
            logRetentionRetryOptions: {
                maxRetries: 100,
            },
        });
        const cluster = new Cluster(this, "Cluster", {
            clusterName,
            vpc,
        });
        const taskDefinition = new FargateTaskDefinition(this, `TaskDefinition`, {
            // @ts-ignore
            memoryLimitMiB: supportedMemories[cpu][memory],
            cpu: supportedCpus[cpu],
        });
        const container = taskDefinition.addContainer("Container", {
            logging: new AwsLogDriver({
                logGroup: LogGroup.fromLogGroupArn(this, "LogGroup", logGroup.logGroupArn),
                streamPrefix: "service",
            }),
            portMappings: [{ containerPort: port }],
            environment: {
                SST_APP: app.name,
                SST_STAGE: app.stage,
                SST_SSM_PREFIX: useProject().config.ssmPrefix,
            },
            ...cdk?.container,
            image: { bind: () => ({ imageName: "placeholder" }) },
        });
        const service = new FargateService(this, "Service", {
            cluster,
            taskDefinition,
        });
        return { cluster, taskDefinition, container, service };
    }
    createLoadBalancer(vpc, service) {
        const alb = new ApplicationLoadBalancer(this, "LoadBalancer", {
            vpc,
            internetFacing: true,
        });
        const listener = alb.addListener("Listener", { port: 80 });
        const target = listener.addTargets("TargetGroup", {
            port: 80,
            targets: [service],
        });
        return { alb, target };
    }
    createAutoScaling(service, target) {
        const { minContainers, maxContainers, cpuUtilization, memoryUtilization, requestsPerContainer, } = this.props.scaling ?? {};
        const scaling = service.autoScaleTaskCount({
            minCapacity: minContainers ?? 1,
            maxCapacity: maxContainers ?? 1,
        });
        scaling.scaleOnCpuUtilization("CpuScaling", {
            targetUtilizationPercent: cpuUtilization ?? 70,
            scaleOutCooldown: CdkDuration.seconds(300),
        });
        scaling.scaleOnMemoryUtilization("MemoryScaling", {
            targetUtilizationPercent: memoryUtilization ?? 70,
            scaleOutCooldown: CdkDuration.seconds(300),
        });
        scaling.scaleOnRequestCount("RequestScaling", {
            requestsPerTarget: requestsPerContainer ?? 500,
            targetGroup: target,
        });
    }
    createDistribution(alb) {
        const { customDomain } = this.props;
        const cachePolicy = new CachePolicy(this, "CachePolicy", {
            queryStringBehavior: CacheQueryStringBehavior.all(),
            headerBehavior: CacheHeaderBehavior.none(),
            cookieBehavior: CacheCookieBehavior.none(),
            defaultTtl: CdkDuration.days(0),
            maxTtl: CdkDuration.days(365),
            minTtl: CdkDuration.days(0),
            enableAcceptEncodingBrotli: true,
            enableAcceptEncodingGzip: true,
            comment: "SST server response cache policy",
        });
        return new Distribution(this, "CDN", {
            customDomain,
            cdk: {
                distribution: {
                    defaultRootObject: "",
                    defaultBehavior: {
                        viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
                        origin: new HttpOrigin(alb.loadBalancerDnsName, {
                            protocolPolicy: OriginProtocolPolicy.HTTP_ONLY,
                            readTimeout: CdkDuration.seconds(60),
                        }),
                        allowedMethods: AllowedMethods.ALLOW_ALL,
                        cachedMethods: CachedMethods.CACHE_GET_HEAD_OPTIONS,
                        compress: true,
                        cachePolicy,
                        originRequestPolicy: OriginRequestPolicy.ALL_VIEWER,
                    },
                },
            },
        });
    }
    createDevFunction() {
        const { permissions, environment, bind } = this.props;
        const app = this.node.root;
        const role = new Role(this, "ServerFunctionRole", {
            assumedBy: new CompositePrincipal(new AccountPrincipal(app.account), new ServicePrincipal("lambda.amazonaws.com")),
            maxSessionDuration: CdkDuration.hours(12),
        });
        return new Function(this, `ServerFunction`, {
            description: "Service dev function",
            handler: path.join(__dirname, "../support/service-dev-function", "index.handler"),
            runtime: "nodejs18.x",
            memorySize: "512 MB",
            timeout: "10 seconds",
            role,
            bind,
            environment,
            permissions,
        });
    }
    bindForService(constructs) {
        // Get referenced secrets
        const referencedSecrets = [];
        constructs.forEach((c) => referencedSecrets.push(...getReferencedSecrets(c)));
        [...constructs, ...referencedSecrets].forEach((c) => {
            // Bind environment
            const env = bindEnvironment(c);
            Object.entries(env).forEach(([key, value]) => this.addEnvironmentForService(key, value));
            // Bind permissions
            const permissions = bindPermissions(c);
            Object.entries(permissions).forEach(([action, resources]) => this.attachPermissionsForService([
                new PolicyStatement({
                    actions: [action],
                    effect: Effect.ALLOW,
                    resources,
                }),
            ]));
        });
    }
    addEnvironmentForService(name, value) {
        this.container.addEnvironment(name, value);
    }
    attachPermissionsForService(permissions) {
        attachPermissionsToRole(this.taskDefinition.taskRole, permissions);
    }
    /////////////////////
    // Build App
    /////////////////////
    async createNixpacksBuilder() {
        try {
            await execAsync([
                "docker",
                "build",
                `-t ${NIXPACKS_IMAGE_NAME}`,
                "--platform=linux/amd64",
                path.resolve(__dirname, "../support/nixpacks"),
            ].join(" "), {
                env: {
                    ...process.env,
                },
            });
        }
        catch (e) {
            console.error(e);
            throw new VisibleError(`Failed to setup Nixpacks builder for the ${this.node.id} service`);
        }
    }
    async runNixpacksBuild() {
        const { path: servicePath } = this.props;
        try {
            await execAsync([
                "docker",
                "run",
                "--rm",
                "--network=host",
                `--name=sst-${this.node.id}-service`,
                `-v=${path.resolve(servicePath)}:/service`,
                `-w="/service"`,
                NIXPACKS_IMAGE_NAME,
                `build . --out .`,
            ].join(" "), {
                env: {
                    ...process.env,
                },
            });
        }
        catch (e) {
            console.error(e);
            throw new VisibleError(`Failed to run Nixpacks build for the ${this.node.id} service`);
        }
        return ".nixpacks/Dockerfile";
    }
    async runDockerBuild(dockerfile) {
        try {
            await execAsync([
                "docker",
                "build",
                `-t sst-build:service-${this.node.id}`,
                "--platform=linux/amd64",
                `-f ${path.join(this.props.path, dockerfile)}`,
                this.props.path,
            ].join(" "), {
                env: {
                    ...process.env,
                },
            });
        }
        catch (e) {
            console.error(e);
            throw new VisibleError(`Failed to build the ${this.node.id} service`);
        }
    }
    updateContainerImage(dockerfile, taskDefinition, container) {
        const image = ContainerImage.fromAsset(this.props.path, {
            platform: Platform.LINUX_AMD64,
            file: dockerfile,
            exclude: [".sst"],
            ignoreMode: IgnoreMode.GLOB,
        });
        const cfnTask = taskDefinition.node.defaultChild;
        cfnTask.addPropertyOverride("ContainerDefinitions.0.Image", image.bind(this, container).imageName);
    }
}
export const useServices = createAppContext(() => {
    const sites = [];
    return {
        add(stack, name, props) {
            sites.push({ stack, name, props });
        },
        get all() {
            return sites;
        },
    };
});
