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 * as logs from "aws-cdk-lib/aws-logs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { logger, DeferBuilder, FunctionBinding, State } from "@serverless-stack/core";
import { Stack } from "./Stack.js";
import { isSSTConstruct, isStackConstruct } from "./Construct.js";
import { Function } from "./Function.js";
import { bindType, bindParameters } from "./util/functionBinding.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 = [];
        /** @internal */
        this.warnings = {};
        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);
        FunctionBinding.setSsmPrefix(deployProps.ssmPrefix);
        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.
     *
     * @deprecated The "addDefaultFunctionConfig" method will be removed in SST v2. Pass Parameters and Secrets in through the "addDefaultFunctionBinding" function. Read more about how to upgrade here — https://docs.sst.dev/upgrade-guide#upgrade-to-v116
     *
     * @example
     * ```js
     * // Change
     * app.addDefaultFunctionConfig([STRIPE_KEY]);
     *
     * // To
     * app.addDefaultFunctionBinding([STRIPE_KEY]);
     * ```
     */
    addDefaultFunctionConfig(config) {
        this.defaultFunctionProps.push({ config });
    }
    /**
     * Binds additional default resources to be applied to all Lambda functions in the app.
     *
     * @example
     * ```js
     * app.addDefaultFunctionBinding([STRIPE_KEY, bucket]);
     * ```
     */
    addDefaultFunctionBinding(bind) {
        this.defaultFunctionProps.push({ bind });
    }
    /**
     * Adds additional default layers to be applied to all Lambda functions in the stack.
     */
    addDefaultFunctionLayers(layers) {
        this.defaultFunctionProps.push({
            layers
        });
    }
    synth(options = {}) {
        Auth.injectConfig();
        this.ensureUniqueConstructIds();
        this.codegenBindingTypes();
        this.createBindingSsmParameters();
        this.buildConstructsMetadata();
        this.removeGovCloudUnsupportedResourceProperties();
        this.printWarnings();
        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;
    }
    async runDeferredBuilds() {
        await DeferBuilder.run();
    }
    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));
    }
    removeGovCloudUnsupportedResourceProperties() {
        if (!this.region.startsWith("us-gov-")) {
            return;
        }
        class RemoveGovCloudUnsupportedResourceProperties {
            visit(node) {
                if (node instanceof lambda.CfnFunction) {
                    node.addPropertyDeletionOverride("EphemeralStorage");
                }
                else if (node instanceof logs.CfnLogGroup) {
                    node.addPropertyDeletionOverride("Tags");
                }
            }
        }
        cdk.Aspects.of(this).add(new RemoveGovCloudUnsupportedResourceProperties());
    }
    ensureUniqueConstructIds() {
        // "ids" has the shape of:
        // {
        //   Table: {
        //     "id_with_hyphen": "id-with-hyphen",
        //     "id_with_underscore": "id_with_underscore",
        //   }
        // }
        const ids = {};
        class EnsureUniqueConstructIds {
            visit(c) {
                if (!isSSTConstruct(c)) {
                    return;
                }
                if (c instanceof Function && c._disableBind) {
                    return;
                }
                const className = c.constructor.name;
                const id = c.id;
                const normId = FunctionBinding.normalizeId(id);
                const existingIds = ids[className] || {};
                if (!id.match(/^[a-zA-Z]([a-zA-Z0-9-_])*$/)) {
                    throw new Error([
                        `Invalid id "${id}" for ${className} construct.`,
                        ``,
                        `Starting v1.16, construct ids can only contain alphabetic characters, hyphens ("-"), and underscores ("_"), and must start with an alphabetic character. If you are migrating from version 1.15 or earlier, please see the upgrade guide — https://docs.sst.dev/upgrade-guide#upgrade-to-v116`,
                    ].join("\n"));
                }
                else if (["Parameter", "Secret"].includes(className)) {
                    const existingConfigId = ids.Secret?.[normId] || ids.Parameter?.[normId];
                    if (existingConfigId === id) {
                        throw new Error(`ERROR: Config with id "${id}" already exists.`);
                    }
                    else if (existingConfigId) {
                        throw new Error(`ERROR: You cannot have the same Config id with an underscore and hyphen: "${existingConfigId}" and "${id}".`);
                    }
                }
                else if (existingIds[normId]) {
                    throw new Error([
                        existingIds[normId] === id
                            ? `${className} with id "${id}" already exists.`
                            : `You cannot have the same ${className} id with an underscore and hyphen: "${existingIds[normId]}" and "${id}".`,
                        ``,
                        `Starting v1.16, constructs must have unique ids for a given construct type. If you are migrating from version 1.15 or earlier, set the "cdk.id" in the construct with the existing id, and pick a unique id for the construct. Please see the upgrade guide — https://docs.sst.dev/upgrade-guide#upgrade-to-v116`,
                        ``,
                        `    For example, if you have two Bucket constructs with the same id:`,
                        `      new Bucket(this, "bucket");`,
                        `      new Bucket(this, "bucket");`,
                        ``,
                        `    Change it to:`,
                        `      new Bucket(this, "usersBucket", {`,
                        `        cdk: {`,
                        `          id: "bucket"`,
                        `        }`,
                        `      });`,
                        `      new Bucket(this, "adminBucket", {`,
                        `        cdk: {`,
                        `          id: "bucket"`,
                        `        }`,
                        `      });`,
                    ].join("\n"));
                }
                existingIds[normId] = id;
                ids[className] = existingIds;
            }
        }
        cdk.Aspects.of(this).add(new EnsureUniqueConstructIds());
    }
    codegenBindingTypes() {
        // Find the node_modules folder to create the type files in
        const nodeModulesPath = this.codegenFindNodeModulesPath();
        if (!nodeModulesPath) {
            return;
        }
        const typesPath = path.resolve(nodeModulesPath, "node_modules", "@types", "serverless-stack__node");
        this.codegenCreateIndexType(typesPath);
        this.codegenCreateConstructTypes(typesPath);
    }
    codegenFindNodeModulesPath() {
        const rootPath = path.parse(process.cwd()).root;
        let directory = this.appPath;
        while (directory !== rootPath) {
            const checkPath = path.resolve(directory, "node_modules", "@serverless-stack", "node");
            if (fs.existsSync(checkPath)) {
                return directory;
            }
            directory = path.dirname(directory);
        }
    }
    codegenCreateIndexType(typesPath) {
        fs.removeSync(typesPath);
        fs.mkdirSync(typesPath, {
            recursive: true,
        });
        fs.writeFileSync(`${typesPath}/index.d.ts`, `
import "@serverless-stack/node/config";
declare module "@serverless-stack/node/config" {
  export interface ConfigTypes {
    APP: string;
    STAGE: string;
  }
}`);
    }
    codegenCreateConstructTypes(typesPath) {
        class CodegenTypes {
            visit(c) {
                if (!isSSTConstruct(c)) {
                    return;
                }
                if (c instanceof Function && c._disableBind) {
                    return;
                }
                const binding = bindType(c);
                if (!binding) {
                    return;
                }
                const className = c.constructor.name;
                const id = c.id;
                fs.appendFileSync(`${typesPath}/index.d.ts`, `export * from "./${className}-${id}";`);
                // Case 1: variable does not have properties, ie. Secrets and Parameters
                const typeContent = binding.variables[0] === "."
                    ? `
import "@serverless-stack/node/${binding.clientPackage}";
declare module "@serverless-stack/node/${binding.clientPackage}" {
  export interface ${className}Resources {
    "${id}": string;
  }
}`
                    : `
import "@serverless-stack/node/${binding.clientPackage}";
declare module "@serverless-stack/node/${binding.clientPackage}" {
  export interface ${className}Resources {
    "${id}": {
      ${binding.variables.map((p) => `${p}: string;`).join("\n")}
    }
  }
}`;
                fs.writeFileSync(`${typesPath}/${className}-${id}.d.ts`, typeContent);
            }
        }
        cdk.Aspects.of(this).add(new CodegenTypes());
    }
    createBindingSsmParameters() {
        class CreateSsmParameters {
            visit(c) {
                if (!isSSTConstruct(c)) {
                    return;
                }
                if (c instanceof Function && c._disableBind) {
                    return;
                }
                bindParameters(c);
            }
        }
        cdk.Aspects.of(this).add(new CreateSsmParameters());
    }
    /** @internal */
    reportWarning(type) {
        this.warnings[type] = true;
    }
    printWarnings() {
        if (this.warnings.usingConfig) {
            logger.warn(`\nWARNING: The "config" prop is deprecated, and will be removed in SST v2. Pass Parameters and Secrets in through the "bind" prop. Read more about how to upgrade here — https://docs.sst.dev/upgrade-guide#upgrade-to-v116`);
        }
        if (this.warnings.usingPermissionsWithSSTConstruct) {
            logger.warn(`\nWARNING: Passing SST constructs into "permissions" is deprecated, and will be removed in SST v2. Pass them into the "bind" prop. Read more about how to upgrade here — https://docs.sst.dev/upgrade-guide#upgrade-to-v116`);
        }
        if (this.warnings.usingApiPothosRoute) {
            logger.warn(`\nWARNING: The "pothos" route type is renamed to "graphql" for the Api construct, and will be removed in SST v2. Read more about how to upgrade here — https://docs.sst.dev/upgrade-guide#upgrade-to-v118`);
        }
        if (this.warnings.usingGraphQLApi) {
            logger.warn(`\nWARNING: The "GraphQLApi" construct is deprecated, and will be removed in SST v2. Use the "Api" construct with a "graphql" route instead. Read more about how to upgrade here — https://docs.sst.dev/upgrade-guide#upgrade-to-v118`);
        }
    }
    // 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);
    }
}
