import path from "path";
import fs from "fs-extra";
import * as cdk from "aws-cdk-lib";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as iam from "aws-cdk-lib/aws-iam";
import { State } from "@serverless-stack/core";
import { Stack } from "./Stack.js";
import { isSSTConstruct, isStackConstruct, } from "./Construct.js";
import * as Config from "./Config.js";
import { stack } from "./FunctionalStack.js";
import { createRequire } from "module";
import { Auth } from "./Auth.js";
const require = createRequire(import.meta.url);
function exitWithMessage(message) {
    console.error(message);
    process.exit(1);
}
/**
 * The App construct extends cdk.App and is used internally by SST.
 */
export class App extends cdk.App {
    /**
     * @internal
     */
    constructor(deployProps = {}, props = {}) {
        super(props);
        /**
         * Whether or not the app is running locally under `sst start`
         */
        this.local = false;
        /**
         * A list of Lambda functions in the app
         */
        this.lambdaHandlers = [];
        this.siteEnvironments = [];
        this.appPath = process.cwd();
        this.stage = deployProps.stage || "dev";
        this.name = deployProps.name || "my-app";
        this.region =
            deployProps.region || process.env.CDK_DEFAULT_REGION || "us-east-1";
        this.account = process.env.CDK_DEFAULT_ACCOUNT || "my-account";
        this.esbuildConfig = deployProps.esbuildConfig;
        this.bootstrapAssets = deployProps.bootstrapAssets || {};
        this.buildDir = deployProps.buildDir || ".build";
        this.skipBuild = deployProps.skipBuild || false;
        this.defaultFunctionProps = [];
        this.synthCallback = deployProps.synthCallback;
        State.init(this.appPath);
        if (deployProps.debugEndpoint) {
            this.local = true;
            State.Function.reset(this.appPath);
            this.debugEndpoint = deployProps.debugEndpoint;
            this.debugBucketArn = deployProps.debugBucketArn;
            this.debugBucketName = deployProps.debugBucketName;
            this.debugStartedAt = deployProps.debugStartedAt;
            this.debugIncreaseTimeout = deployProps.debugIncreaseTimeout;
            if (deployProps.debugBridge) {
                this.debugBridge = deployProps.debugBridge;
            }
        }
    }
    /** @internal */
    get defaultRemovalPolicy() {
        return this._defaultRemovalPolicy;
    }
    /**
     * Use this method to prefix resource names in your stacks to make sure they don't thrash when deployed to different stages in the same AWS account. This method will prefix a given resource name with the stage and app name. Using the format `${stage}-${name}-${logicalName}`.
     * @example
     * ```js
     * console.log(app.logicalPrefixedName("myTopic"));
     *
     * // dev-my-app-myTopic
     * ```
     */
    logicalPrefixedName(logicalName) {
        const namePrefix = this.name === "" ? "" : `${this.name}-`;
        return `${this.stage}-${namePrefix}${logicalName}`;
    }
    /**
     * The default removal policy that'll be applied to all the resources in the app. This can be useful to set ephemeral (dev or feature branch) environments to remove all the resources on deletion.
     * :::danger
     * Make sure to not set the default removal policy to `DESTROY` for production environments.
     * :::
     * @example
     * ```js
     * app.setDefaultRemovalPolicy(app.local ? "destroy" : "retain")
     * ```
     */
    setDefaultRemovalPolicy(policy) {
        this._defaultRemovalPolicy = policy;
    }
    /**
     * The default function props to be applied to all the Lambda functions in the app. These default values will be overridden if a Function sets its own props.
     * This needs to be called before a stack with any functions have been added to the app.
     *
     * @example
     * ```js
     * app.setDefaultFunctionProps({
     *   runtime: "nodejs12.x",
     *   timeout: 30
     * })
     * ```
     */
    setDefaultFunctionProps(props) {
        if (this.lambdaHandlers.length > 0)
            throw new Error("Cannot call 'setDefaultFunctionProps' after a stack with functions has been created. Please use 'addDefaultFunctionEnv' or 'addDefaultFunctionPermissions' to add more default properties. Read more about this change here: https://docs.sst.dev/constructs/App#upgrading-to-v0420");
        this.defaultFunctionProps.push(props);
    }
    /**
     * Adds additional default Permissions to be applied to all Lambda functions in the app.
     *
     * @example
     * ```js
     * app.addDefaultFunctionPermissions(["s3"])
     * ```
     */
    addDefaultFunctionPermissions(permissions) {
        this.defaultFunctionProps.push({
            permissions,
        });
    }
    /**
     * Adds additional default environment variables to be applied to all Lambda functions in the app.
     *
     * @example
     * ```js
     * app.addDefaultFunctionPermissions({
     *   "MY_ENV_VAR": "my-value"
     * })
     * ```
     */
    addDefaultFunctionEnv(environment) {
        this.defaultFunctionProps.push({
            environment,
        });
    }
    /**
     * Adds additional default config to be applied to all Lambda functions in the app.
     *
     * @example
     * ```js
     * app.addDefaultFunctionConfig([STRIPE_KEY])
     * ```
     */
    addDefaultFunctionConfig(config) {
        this.defaultFunctionProps.push({ config });
    }
    /**
     * Adds additional default layers to be applied to all Lambda functions in the stack.
     */
    addDefaultFunctionLayers(layers) {
        this.defaultFunctionProps.push({
            layers,
        });
    }
    synth(options = {}) {
        this.createTypesFile();
        Config.codegenTypes();
        this.buildConstructsMetadata();
        Auth.injectConfig();
        for (const child of this.node.children) {
            if (isStackConstruct(child)) {
                // Tag stacks
                cdk.Tags.of(child).add("sst:app", this.name);
                cdk.Tags.of(child).add("sst:stage", this.stage);
                // Set removal policy
                if (this._defaultRemovalPolicy)
                    this.applyRemovalPolicy(child, this._defaultRemovalPolicy);
                // Stack names need to be parameterized with the stage name
                if (!child.stackName.startsWith(`${this.stage}-`) &&
                    !child.stackName.endsWith(`-${this.stage}`) &&
                    child.stackName.indexOf(`-${this.stage}-`) === -1) {
                    throw new Error(`Stack "${child.stackName}" is not parameterized with the stage name. The stack name needs to either start with "$stage-", end in "-$stage", or contain the stage name "-$stage-".`);
                }
            }
        }
        const cloudAssembly = super.synth(options);
        // Run callback after synth has finished
        if (this.synthCallback) {
            this.synthCallback(this.lambdaHandlers, this.siteEnvironments);
        }
        return cloudAssembly;
    }
    isRunningSSTTest() {
        // Check the env var set inside test/setup-tests.js
        return process.env.SST_RESOURCES_TESTS === "enabled";
    }
    registerLambdaHandler(handler) {
        this.lambdaHandlers.push(handler);
    }
    registerSiteEnvironment(environment) {
        this.siteEnvironments.push(environment);
    }
    getInputFilesFromEsbuildMetafile(file) {
        let metaJson;
        try {
            metaJson = fs.readJsonSync(file);
        }
        catch (e) {
            exitWithMessage("There was a problem reading the esbuild metafile.");
        }
        return Object.keys(metaJson.inputs).map((input) => path.resolve(input));
    }
    buildConstructsMetadata() {
        const constructs = this.buildConstructsMetadata_collectConstructs(this);
        const byStack = {};
        const local = [];
        for (const c of constructs) {
            const stack = Stack.of(c);
            const list = byStack[stack.node.id] || [];
            const metadata = c.getConstructMetadata();
            const item = {
                id: c.node.id,
                addr: c.node.addr,
                stack: Stack.of(c).stackName,
                ...metadata,
            };
            local.push(item);
            list.push({
                ...item,
                local: undefined,
            });
            byStack[stack.node.id] = list;
        }
        // Register constructs
        for (const child of this.node.children) {
            if (child instanceof Stack) {
                const stackName = child.node.id;
                child.setStackMetadata(byStack[stackName] || []);
            }
        }
        fs.writeJSONSync(State.resolve(this.appPath, "constructs.json"), local);
    }
    buildConstructsMetadata_collectConstructs(construct) {
        return [
            isSSTConstruct(construct) ? construct : undefined,
            ...construct.node.children.flatMap((c) => this.buildConstructsMetadata_collectConstructs(c)),
        ].filter((c) => Boolean(c));
    }
    applyRemovalPolicy(current, policy) {
        if (current instanceof cdk.CfnResource) {
            current.applyRemovalPolicy(cdk.RemovalPolicy[policy.toUpperCase()]);
        }
        // Had to copy this in to enable deleting objects in bucket
        // https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-s3/lib/bucket.ts#L1910
        if (current instanceof s3.Bucket &&
            !current.node.tryFindChild("AutoDeleteObjectsCustomResource")) {
            const AUTO_DELETE_OBJECTS_RESOURCE_TYPE = "Custom::S3AutoDeleteObjects";
            const provider = cdk.CustomResourceProvider.getOrCreateProvider(current, AUTO_DELETE_OBJECTS_RESOURCE_TYPE, {
                codeDirectory: path.join(require.resolve("aws-cdk-lib/aws-s3"), "../lib/auto-delete-objects-handler"),
                runtime: cdk.CustomResourceProviderRuntime.NODEJS_16_X,
                description: `Lambda function for auto-deleting objects in ${current.bucketName} S3 bucket.`,
            });
            // Use a bucket policy to allow the custom resource to delete
            // objects in the bucket
            current.addToResourcePolicy(new iam.PolicyStatement({
                actions: [
                    // list objects
                    "s3:GetBucket*",
                    "s3:List*",
                    // and then delete them
                    "s3:DeleteObject*",
                ],
                resources: [current.bucketArn, current.arnForObjects("*")],
                principals: [new iam.ArnPrincipal(provider.roleArn)],
            }));
            const customResource = new cdk.CustomResource(current, "AutoDeleteObjectsCustomResource", {
                resourceType: AUTO_DELETE_OBJECTS_RESOURCE_TYPE,
                serviceToken: provider.serviceToken,
                properties: {
                    BucketName: current.bucketName,
                },
            });
            // Ensure bucket policy is deleted AFTER the custom resource otherwise
            // we don't have permissions to list and delete in the bucket.
            // (add a `if` to make TS happy)
            if (current.policy) {
                customResource.node.addDependency(current.policy);
            }
        }
        current.node.children.forEach((resource) => this.applyRemovalPolicy(resource, policy));
    }
    createTypesFile() {
        fs.removeSync("node_modules/@types/serverless-stack__node");
        fs.mkdirSync("node_modules/@types/serverless-stack__node", {
            recursive: true,
        });
        fs.writeFileSync("node_modules/@types/serverless-stack__node/index.d.ts", "");
    }
    // Functional Stack
    // This is a magical global to avoid having to pass app everywhere.
    // We only every have one instance of app
    stack(fn, props) {
        return stack(this, fn, props);
    }
}
