import url from "url";
import path from "path";
import { bold, dim } from "colorette";
import { spawn } from "child_process";
import { DescribeStacksCommand, CloudFormationClient, } from "@aws-sdk/client-cloudformation";
import { App, DefaultStackSynthesizer, Duration, CfnOutput, Tags, Stack, RemovalPolicy, } from "aws-cdk-lib/core";
import { Function, Runtime, Code } from "aws-cdk-lib/aws-lambda";
import { ManagedPolicy, PermissionsBoundary, PolicyStatement, } from "aws-cdk-lib/aws-iam";
import { Rule } from "aws-cdk-lib/aws-events";
import { LambdaFunction } from "aws-cdk-lib/aws-events-targets";
import { BlockPublicAccess, Bucket, BucketEncryption, } from "aws-cdk-lib/aws-s3";
import { useProject } from "./project.js";
import { createSpinner } from "./cli/spinner.js";
import { Context } from "./context/context.js";
import { useAWSClient, useAWSCredentials, useSTSIdentity, } from "./credentials.js";
import { VisibleError } from "./error.js";
import { Logger } from "./logger.js";
import { Stacks } from "./stacks/index.js";
const CDK_STACK_NAME = "CDKToolkit";
const SST_STACK_NAME = "SSTBootstrap";
const OUTPUT_VERSION = "Version";
const OUTPUT_BUCKET = "BucketName";
const LATEST_VERSION = "7.2";
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
export const useBootstrap = Context.memo(async () => {
    Logger.debug("Initializing bootstrap context");
    let [cdkStatus, sstStatus] = await Promise.all([
        loadCDKStatus(),
        loadSSTStatus(),
    ]);
    Logger.debug("Loaded bootstrap status");
    const needToBootstrapCDK = cdkStatus.status !== "ready";
    const needToBootstrapSST = sstStatus.status !== "ready";
    if (needToBootstrapCDK || needToBootstrapSST) {
        const spinner = createSpinner(cdkStatus.status === "bootstrap" || sstStatus.status === "bootstrap"
            ? "Deploying bootstrap stack, this only needs to happen once"
            : "Updating bootstrap stack").start();
        if (needToBootstrapCDK) {
            await bootstrapCDK();
            // fetch bootstrap status
            cdkStatus = await loadCDKStatus();
            if (cdkStatus.status !== "ready")
                throw new VisibleError("Failed to load bootstrap stack status");
        }
        if (needToBootstrapSST) {
            await bootstrapSST(cdkStatus.bucket);
            // fetch bootstrap status
            sstStatus = await loadSSTStatus();
            if (sstStatus.status !== "ready")
                throw new VisibleError("Failed to load bootstrap stack status");
        }
        spinner.succeed();
    }
    Logger.debug("Bootstrap context initialized", sstStatus);
    return sstStatus;
}, "Bootstrap");
async function loadCDKStatus() {
    const { cdk } = useProject().config;
    const client = useAWSClient(CloudFormationClient);
    const stackName = cdk?.toolkitStackName || CDK_STACK_NAME;
    try {
        const { Stacks: stacks } = await client.send(new DescribeStacksCommand({ StackName: stackName }));
        // Check CDK bootstrap stack exists
        if (!stacks || stacks.length === 0)
            return { status: "bootstrap" };
        // Check CDK bootstrap stack deployed successfully
        if (![
            "CREATE_COMPLETE",
            "UPDATE_COMPLETE",
            "UPDATE_ROLLBACK_COMPLETE",
        ].includes(stacks[0].StackStatus)) {
            return { status: "bootstrap" };
        }
        // Check CDK bootstrap stack is up to date
        // note: there is no a programmatical way to get the minimal required version
        //       of CDK bootstrap stack. We are going to hardcode it to 14 for now,
        //       which is the latest version as of CDK v2.62.2
        let version;
        let bucket;
        const output = stacks[0].Outputs?.forEach((o) => {
            if (o.OutputKey === "BootstrapVersion") {
                version = parseInt(o.OutputValue);
            }
            else if (o.OutputKey === "BucketName") {
                bucket = o.OutputValue;
            }
        });
        if (!version || version < 14 || !bucket) {
            return { status: "update" };
        }
        return { status: "ready", version, bucket };
    }
    catch (e) {
        if (e.name === "ValidationError" &&
            e.message === `Stack with id ${stackName} does not exist`) {
            return { status: "bootstrap" };
        }
        else {
            throw e;
        }
    }
}
async function loadSSTStatus() {
    // Get bootstrap CloudFormation stack
    const { bootstrap } = useProject().config;
    const cf = useAWSClient(CloudFormationClient);
    const stackName = bootstrap?.stackName || SST_STACK_NAME;
    let result;
    try {
        result = await cf.send(new DescribeStacksCommand({
            StackName: stackName,
        }));
    }
    catch (e) {
        if (e.Code === "ValidationError" &&
            e.message === `Stack with id ${stackName} does not exist`) {
            return { status: "bootstrap" };
        }
        throw e;
    }
    // Parse stack outputs
    let version;
    let bucket;
    (result.Stacks[0].Outputs || []).forEach((o) => {
        if (o.OutputKey === OUTPUT_VERSION) {
            version = o.OutputValue;
        }
        else if (o.OutputKey === OUTPUT_BUCKET) {
            bucket = o.OutputValue;
        }
    });
    if (!version || !bucket) {
        return { status: "bootstrap" };
    }
    // Need to update bootstrap stack:
    // 1. If current MAJOR version < latest MAJOR version
    // 2. If current MAJOR version > latest MAJOR version (has breaking change)
    // 3. If current MAJOR version == latest MAJOR version,
    //    but current MINOR version < latest MINOR version
    const latestParts = LATEST_VERSION.split(".");
    const latestMajor = parseInt(latestParts[0]);
    const latestMinor = parseInt(latestParts[1] || "0");
    const currentParts = version.split(".");
    const currentMajor = parseInt(currentParts[0]);
    const currentMinor = parseInt(currentParts[1] || "0");
    if (currentMajor < latestMajor ||
        currentMajor > latestMajor ||
        currentMinor < latestMinor) {
        return { status: "update" };
    }
    return { status: "ready", version, bucket };
}
export async function bootstrapSST(cdkBucket) {
    const { region, bootstrap, cdk } = useProject().config;
    // Create bootstrap stack
    const app = new App();
    const stackName = bootstrap?.stackName || SST_STACK_NAME;
    const stack = new Stack(app, stackName, {
        env: {
            region,
        },
        synthesizer: new DefaultStackSynthesizer({
            qualifier: cdk?.qualifier,
            fileAssetsBucketName: cdk?.fileAssetsBucketName,
            deployRoleArn: cdk?.deployRoleArn,
            fileAssetPublishingRoleArn: cdk?.fileAssetPublishingRoleArn,
            imageAssetPublishingRoleArn: cdk?.imageAssetPublishingRoleArn,
            cloudFormationExecutionRole: cdk?.cloudFormationExecutionRole,
            lookupRoleArn: cdk?.lookupRoleArn,
        }),
    });
    // Add tags to stack
    for (const [key, value] of Object.entries(bootstrap?.tags || {})) {
        Tags.of(app).add(key, value);
    }
    // Create S3 bucket to store stacks metadata
    const bucket = bootstrap?.useCdkBucket
        ? {
            bucketName: cdkBucket,
            bucketArn: `arn:${stack.partition}:s3:::${cdkBucket}`,
        }
        : new Bucket(stack, region, {
            encryption: BucketEncryption.S3_MANAGED,
            removalPolicy: RemovalPolicy.DESTROY,
            autoDeleteObjects: true,
            enforceSSL: true,
            lifecycleRules: [
                {
                    id: "Remove partial uploads after 3 days",
                    enabled: true,
                    abortIncompleteMultipartUploadAfter: Duration.days(3),
                },
            ],
            blockPublicAccess: cdk?.publicAccessBlockConfiguration !== false
                ? BlockPublicAccess.BLOCK_ALL
                : undefined,
        });
    // Create Function and subscribe to CloudFormation events
    const fn = new Function(stack, "MetadataHandler", {
        code: Code.fromAsset(path.resolve(__dirname, "support/bootstrap-metadata-function")),
        handler: "index.handler",
        runtime: region?.startsWith("us-gov-")
            ? Runtime.NODEJS_16_X
            : Runtime.NODEJS_18_X,
        environment: {
            BUCKET_NAME: bucket.bucketName,
        },
        initialPolicy: [
            new PolicyStatement({
                actions: ["cloudformation:DescribeStacks"],
                resources: ["*"],
            }),
            new PolicyStatement({
                actions: ["s3:PutObject", "s3:DeleteObject"],
                resources: [bucket.bucketArn + "/*"],
            }),
            new PolicyStatement({
                actions: ["iot:Publish"],
                resources: [
                    `arn:${stack.partition}:iot:${stack.region}:${stack.account}:topic//sst/*`,
                ],
            }),
        ],
    });
    const rule = new Rule(stack, "MetadataRule", {
        eventPattern: {
            source: ["aws.cloudformation"],
            detailType: ["CloudFormation Stack Status Change"],
            detail: {
                "status-details": {
                    status: [
                        "CREATE_COMPLETE",
                        "UPDATE_COMPLETE",
                        "UPDATE_ROLLBACK_COMPLETE",
                        "ROLLBACK_COMPLETE",
                        "DELETE_COMPLETE",
                    ],
                },
            },
        },
    });
    rule.addTarget(new LambdaFunction(fn));
    // Create permissions boundary
    if (cdk?.customPermissionsBoundary) {
        const boundaryPolicy = ManagedPolicy.fromManagedPolicyName(stack, "PermissionBoundaryPolicy", cdk.customPermissionsBoundary);
        PermissionsBoundary.of(stack).apply(boundaryPolicy);
    }
    // Create stack outputs to store bootstrap stack info
    new CfnOutput(stack, OUTPUT_VERSION, { value: LATEST_VERSION });
    new CfnOutput(stack, OUTPUT_BUCKET, { value: bucket.bucketName });
    // Deploy bootstrap stack
    const asm = app.synth();
    const result = await Stacks.deploy(asm.stacks[0]);
    if (Stacks.isFailed(result.status)) {
        throw new VisibleError(`Failed to deploy bootstrap stack:\n${JSON.stringify(result.errors, null, 4)}`);
    }
}
async function bootstrapCDK() {
    const identity = await useSTSIdentity();
    const credentials = await useAWSCredentials();
    const { region, profile, cdk } = useProject().config;
    await new Promise((resolve, reject) => {
        const proc = spawn([
            "npx",
            "cdk",
            "bootstrap",
            `aws://${identity.Account}/${region}`,
            "--no-version-reporting",
            ...(cdk?.publicAccessBlockConfiguration === false
                ? ["--public-access-block-configuration", "false"]
                : cdk?.publicAccessBlockConfiguration === true
                    ? ["--public-access-block-configuration", "false"]
                    : []),
            ...(cdk?.toolkitStackName
                ? ["--toolkit-stack-name", cdk.toolkitStackName]
                : []),
            ...(cdk?.qualifier ? ["--qualifier", cdk.qualifier] : []),
            ...(cdk?.fileAssetsBucketName
                ? ["--toolkit-bucket-name", cdk.fileAssetsBucketName]
                : []),
            ...(cdk?.customPermissionsBoundary
                ? ["--custom-permissions-boundary", cdk.customPermissionsBoundary]
                : []),
        ].join(" "), {
            env: {
                ...process.env,
                AWS_ACCESS_KEY_ID: credentials.accessKeyId,
                AWS_SECRET_ACCESS_KEY: credentials.secretAccessKey,
                AWS_SESSION_TOKEN: credentials.sessionToken,
                AWS_REGION: region,
                AWS_PROFILE: profile,
            },
            stdio: "pipe",
            shell: true,
        });
        let stderr = "";
        proc.stdout.on("data", (data) => {
            Logger.debug(data.toString());
        });
        proc.stderr.on("data", (data) => {
            Logger.debug(data.toString());
            stderr += data;
        });
        proc.on("exit", (code) => {
            Logger.debug("CDK bootstrap exited with code " + code);
            if (code === 0) {
                resolve();
            }
            else {
                console.log(bold(dim(stderr)));
                reject(new VisibleError(`Failed to bootstrap`));
            }
        });
    });
}
