import path from "path";
import glob from "glob";
import fs from "fs-extra";
import * as crypto from "crypto";
import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as rds from "aws-cdk-lib/aws-rds";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { getFunctionRef, isCDKConstruct } from "./Construct.js";
import { Function as Fn } from "./Function.js";
import url from "url";
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
/////////////////////
// Construct
/////////////////////
/**
 * The `RDS` construct is a higher level CDK construct that makes it easy to create an [RDS Serverless Cluster](https://aws.amazon.com/rds/).
 *
 * @example
 *
 * ```js
 * import { RDS } from "@serverless-stack/resources";
 *
 * new RDS(stack, "Database", {
 *   engine: "postgresql11.13",
 *   defaultDatabaseName: "my_database",
 * });
 * ```
 */
export class RDS extends Construct {
    constructor(scope, id, props) {
        super(scope, id);
        this.validateRequiredProps(props);
        this.cdk = {};
        this.props = props || {};
        const { migrations, cdk } = this.props;
        // Create the cluster
        if (cdk && isCDKConstruct(cdk.cluster)) {
            this.validateCDKPropWhenIsConstruct();
            this.cdk.cluster = this.importCluster();
            this.secret = cdk.secret;
        }
        else {
            this.validateCDKPropWhenIsClusterProps();
            this.cdk.cluster = this.createCluster();
            this.secret = this.cdk.cluster.secret;
        }
        // Create the migrator function
        if (migrations) {
            this.validateMigrationsFileExists(migrations);
            this.createMigrationsFunction(migrations);
            this.createMigrationCustomResource(migrations);
        }
    }
    /**
     * The ARN of the internally created RDS Serverless Cluster.
     */
    get clusterArn() {
        return this.cdk.cluster.clusterArn;
    }
    /**
     * The ARN of the internally created RDS Serverless Cluster.
     */
    get clusterIdentifier() {
        return this.cdk.cluster.clusterIdentifier;
    }
    /**
     * The ARN of the internally created RDS Serverless Cluster.
     */
    get clusterEndpoint() {
        return this.cdk.cluster.clusterEndpoint;
    }
    /**
     * The default database name of the RDS Serverless Cluster.
     */
    get defaultDatabaseName() {
        return this.props.defaultDatabaseName;
    }
    /**
     * The ARN of the internally created Secrets Manager Secret.
     */
    get secretArn() {
        return this.secret.secretArn;
    }
    getConstructMetadata() {
        const { engine, defaultDatabaseName, types } = this.props;
        return {
            type: "RDS",
            data: {
                engine,
                secretArn: this.secretArn,
                types: typeof types === "string"
                    ? {
                        path: types,
                    }
                    : types,
                clusterArn: this.clusterArn,
                clusterIdentifier: this.clusterIdentifier,
                defaultDatabaseName,
                migrator: this.migratorFunction && getFunctionRef(this.migratorFunction),
            },
        };
    }
    validateRequiredProps(props) {
        if (!props.engine) {
            throw new Error(`Missing "engine" in the "${this.node.id}" RDS`);
        }
        if (!props.defaultDatabaseName) {
            throw new Error(`Missing "defaultDatabaseName" in the "${this.node.id}" RDS`);
        }
    }
    validateCDKPropWhenIsConstruct() {
        const { cdk } = this.props;
        if (!cdk?.secret) {
            throw new Error(`Missing "cdk.secret" in the "${this.node.id}" RDS. You must provide a secret to import an existing RDS Serverless Cluster.`);
        }
    }
    validateCDKPropWhenIsClusterProps() {
        const { cdk } = this.props;
        const props = (cdk?.cluster || {});
        // Validate "engine" is passed in from the top level
        if (props.engine) {
            throw new Error(`Use "engine" instead of "cdk.cluster.engine" to configure the RDS database engine.`);
        }
        // Validate "defaultDatabaseName" is passed in from the top level
        if (props.defaultDatabaseName) {
            throw new Error(`Use "defaultDatabaseName" instead of "cdk.cluster.defaultDatabaseName" to configure the RDS database engine.`);
        }
        // Validate "scaling" is passed in from the top level
        if (props.scaling) {
            throw new Error(`Use "scaling" instead of "cdk.cluster.scaling" to configure the RDS database auto-scaling.`);
        }
        // Validate "enableDataApi" is not passed in
        if (props.enableDataApi === false) {
            throw new Error(`Do not configure the "cdk.cluster.enableDataApi". Data API is always enabled for this construct.`);
        }
        // Validate Secrets Manager is used for "credentials"
        if (props.credentials && !props.credentials.secret) {
            throw new Error(`Only credentials managed by SecretManager are supported for the "cdk.cluster.credentials".`);
        }
        return props;
    }
    validateMigrationsFileExists(migrations) {
        if (!fs.existsSync(migrations))
            throw new Error(`Cannot find the migrations in "${path.resolve(migrations)}".`);
    }
    getEngine(engine) {
        if (engine === "mysql5.6") {
            return rds.DatabaseClusterEngine.aurora({
                version: rds.AuroraEngineVersion.VER_10A,
            });
        }
        else if (engine === "mysql5.7") {
            return rds.DatabaseClusterEngine.auroraMysql({
                version: rds.AuroraMysqlEngineVersion.VER_2_07_1,
            });
        }
        else if (engine === "postgresql10.14") {
            return rds.DatabaseClusterEngine.auroraPostgres({
                version: rds.AuroraPostgresEngineVersion.VER_10_14,
            });
        }
        else if (engine === "postgresql11.13") {
            return rds.DatabaseClusterEngine.auroraPostgres({
                version: rds.AuroraPostgresEngineVersion.VER_11_13,
            });
        }
        throw new Error(`The specified "engine" is not supported for sst.RDS. Only mysql5.6, mysql5.7, postgresql10.14, and postgresql11.13 engines are currently supported.`);
    }
    getScaling(scaling) {
        return {
            autoPause: scaling?.autoPause === false
                ? cdk.Duration.minutes(0)
                : scaling?.autoPause === true || scaling?.autoPause === undefined
                    ? cdk.Duration.minutes(5)
                    : cdk.Duration.minutes(scaling?.autoPause),
            minCapacity: rds.AuroraCapacityUnit[scaling?.minCapacity || "ACU_2"],
            maxCapacity: rds.AuroraCapacityUnit[scaling?.maxCapacity || "ACU_16"],
        };
    }
    getVpc(props) {
        if (props.vpc) {
            return props.vpc;
        }
        return new ec2.Vpc(this, "vpc", {
            natGateways: 0,
        });
    }
    getVpcSubnets(props) {
        if (props.vpc) {
            return props.vpcSubnets;
        }
        return {
            subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        };
    }
    createCluster() {
        const { engine, defaultDatabaseName, scaling, cdk } = this.props;
        const app = this.node.root;
        const clusterProps = (cdk?.cluster || {});
        return new rds.ServerlessCluster(this, "Cluster", {
            clusterIdentifier: app.logicalPrefixedName(this.node.id),
            ...clusterProps,
            defaultDatabaseName: defaultDatabaseName,
            enableDataApi: true,
            engine: this.getEngine(engine),
            scaling: this.getScaling(scaling),
            vpc: this.getVpc(clusterProps),
            vpcSubnets: this.getVpcSubnets(clusterProps),
        });
    }
    importCluster() {
        const { cdk } = this.props;
        return cdk.cluster;
    }
    createMigrationsFunction(migrations) {
        const { engine, defaultDatabaseName } = this.props;
        const app = this.node.root;
        // path to migration scripts inside the Lambda function
        const migrationsDestination = "sst_rds_migration_scripts";
        // fullpath of the migrator Lambda function
        // Note:
        // - when invoked from `sst build`, __dirname is `resources/dist`
        // - when running resources tests, __dirname is `resources/src`
        // For now we will do `__dirname/../dist` to make both cases work.
        const srcPath = path.resolve(path.join(__dirname, "../dist/RDS_migrator"));
        const fn = new Fn(this, "MigrationFunction", {
            srcPath,
            handler: "index.handler",
            runtime: "nodejs16.x",
            timeout: 900,
            memorySize: 1024,
            environment: {
                RDS_ARN: this.cdk.cluster.clusterArn,
                RDS_SECRET: this.cdk.cluster.secret.secretArn,
                RDS_DATABASE: defaultDatabaseName,
                RDS_ENGINE_MODE: engine.includes("postgres") ? "postgres" : "mysql",
                // for live development, perserve the migrations path so the migrator
                // can locate the migration files
                RDS_MIGRATIONS_PATH: app.local ? migrations : migrationsDestination,
            },
            bundle: {
                nodeModules: ["kysely", "kysely-data-api"],
                format: "esm",
                // Note that we need to generate a relative path of the migrations off the
                // srcPath because sst.Function internally builds the copy "from" path by
                // joining the srcPath and the from path.
                copyFiles: [
                    {
                        from: path.relative(path.resolve(srcPath), path.resolve(migrations)),
                        to: migrationsDestination,
                    },
                ],
            },
        });
        fn.attachPermissions([this.cdk.cluster]);
        this.migratorFunction = fn;
    }
    createMigrationCustomResource(migrations) {
        const app = this.node.root;
        // Create custom resource handler
        const handler = new lambda.Function(this, "MigrationHandler", {
            code: lambda.Code.fromAsset(path.join(__dirname, "Script")),
            runtime: lambda.Runtime.NODEJS_16_X,
            handler: "index.handler",
            timeout: cdk.Duration.minutes(15),
            memorySize: 1024,
        });
        this.migratorFunction?.grantInvoke(handler);
        // Note: "MigrationsHash" is generated to ensure the Custom Resource function
        //       is only run when migration files change.
        //
        //       Do not use the hash in Live mode, b/c we want the custom resource
        //       to remain the same in CloudFormation template when rebuilding
        //       infrastructure. Otherwise, there will always be a change when
        //       rebuilding infrastructure b/c the "BuildAt" property changes on
        //       each build.
        const hash = app.local ? 0 : this.generateMigrationsHash(migrations);
        new cdk.CustomResource(this, "MigrationResource", {
            serviceToken: handler.functionArn,
            resourceType: "Custom::SSTScript",
            properties: {
                UserCreateFunction: app.local
                    ? undefined
                    : this.migratorFunction?.functionName,
                UserUpdateFunction: app.local
                    ? undefined
                    : this.migratorFunction?.functionName,
                UserParams: JSON.stringify({}),
                MigrationsHash: hash,
            },
        });
    }
    generateMigrationsHash(migrations) {
        // Get all files inside the migrations folder
        const files = glob.sync("**", {
            dot: true,
            nodir: true,
            follow: true,
            cwd: migrations,
        });
        // Calculate hash of all files content
        return crypto
            .createHash("md5")
            .update(files
            .map((file) => fs.readFileSync(path.join(migrations, file)))
            .join(""))
            .digest("hex");
    }
}
