import { Resource } from "../resource.js";
import { ignore } from "../util/ignore.js";
import { logger } from "../util/logger.js";
import { importPeer } from "../util/peer.js";
import { retry } from "./retry.js";
/**
 * AWS Lambda Function Resource
 *
 * Creates and manages AWS Lambda functions with support for Node.js runtimes, custom handlers,
 * environment variables, and function URLs. Handles deployment packaging, IAM role
 * stabilization, and function updates.
 *
 * @example
 * // Create a basic Lambda function with minimal configuration
 * const basicFunction = await Function("api-handler", {
 *   functionName: "api-handler",
 *   zipPath: "./dist/api.zip",
 *   roleArn: role.arn,
 *   runtime: Runtime.nodejs20x,
 *   handler: "index.handler",
 *   tags: {
 *     Environment: "production"
 *   }
 * });
 *
 * @example
 * // Create a function with environment variables and custom memory/timeout
 * const configuredFunction = await Function("worker", {
 *   functionName: "worker",
 *   zipPath: "./dist/worker.zip",
 *   roleArn: role.arn,
 *   runtime: Runtime.nodejs20x,
 *   handler: "worker.process",
 *   memorySize: 512,
 *   timeout: 30,
 *   environment: {
 *     QUEUE_URL: queue.url,
 *     LOG_LEVEL: "info"
 *   }
 * });
 *
 * @example
 * // Create a function with a public URL endpoint, CORS and optional response streaming
 * const apiFunction = await Function("public-api", {
 *   functionName: "public-api",
 *   zipPath: "./dist/api.zip",
 *   roleArn: role.arn,
 *   handler: "api.handler",
 *   url: {
 *     authType: "NONE",
 *     invokeMode: "RESPONSE_STREAM",
 *     cors: {
 *       allowOrigins: ["*"],
 *       allowMethods: ["GET", "POST"],
 *       allowHeaders: ["content-type"],
 *       maxAge: 86400
 *     }
 *   }
 * });
 */
export const Function = Resource("lambda::Function", async function (id, props) {
    const { AddPermissionCommand, Architecture, CreateFunctionCommand, CreateFunctionUrlConfigCommand, DeleteFunctionCommand, DeleteFunctionUrlConfigCommand, GetFunctionCommand, GetFunctionConfigurationCommand, GetFunctionUrlConfigCommand, LambdaClient, ResourceNotFoundException, Runtime, UpdateFunctionCodeCommand, UpdateFunctionConfigurationCommand, UpdateFunctionUrlConfigCommand, } = await importPeer(import("@aws-sdk/client-lambda"), "lambda::Function");
    const client = new LambdaClient({});
    const region = await resolveRegion(client);
    const functionName = props.functionName ??
        this.output?.functionName ??
        this.scope.createPhysicalName(id);
    if (this.phase === "update" && this.output.functionName !== functionName) {
        this.replace();
    }
    if (this.phase === "delete") {
        // Delete function URL if it exists
        if (this.output?.url) {
            try {
                await retry(() => client.send(new DeleteFunctionUrlConfigCommand({
                    FunctionName: functionName,
                })));
            }
            catch (error) {
                if (error.name !== "ResourceNotFoundException") {
                    logger.warn("Failed to delete function URL:", error);
                }
            }
        }
        await ignore(ResourceNotFoundException.name, () => retry(() => client.send(new DeleteFunctionCommand({
            FunctionName: functionName,
        }))));
        return this.destroy();
    }
    const code = await zipCode(props);
    let functionUrl;
    try {
        // Check if function exists
        await retry(() => client.send(new GetFunctionCommand({
            FunctionName: functionName,
        })));
        if (this.phase === "update") {
            // Wait for function to stabilize
            await waitForFunctionStabilization(client, functionName);
            // Update function code
            await retry(() => client.send(new UpdateFunctionCodeCommand({
                FunctionName: functionName,
                ZipFile: code,
            })));
            // Wait for code update to stabilize
            await waitForFunctionStabilization(client, functionName);
            // Update function configuration
            await retry(() => client.send(new UpdateFunctionConfigurationCommand({
                FunctionName: functionName,
                Handler: props.handler,
                Runtime: props.runtime,
                Role: props.roleArn,
                Description: props.description,
                Timeout: props.timeout,
                MemorySize: props.memorySize,
                Environment: props.environment
                    ? { Variables: props.environment }
                    : undefined,
                Layers: props.layers,
            })));
            // Wait for configuration update to stabilize
            await waitForFunctionStabilization(client, functionName);
            // Handle URL configuration
            if (props.url) {
                try {
                    // Check if URL config exists already
                    const urlConfig = await retry(() => client.send(new GetFunctionUrlConfigCommand({
                        FunctionName: functionName,
                    })));
                    // Update URL configuration if it exists
                    if (urlConfig) {
                        const updateResult = await retry(() => client.send(new UpdateFunctionUrlConfigCommand({
                            FunctionName: functionName,
                            AuthType: props.url.authType || "NONE",
                            InvokeMode: props.url.invokeMode || "BUFFERED",
                            Cors: props.url.cors
                                ? {
                                    AllowCredentials: props.url.cors.allowCredentials,
                                    AllowHeaders: props.url.cors.allowHeaders,
                                    AllowMethods: props.url.cors.allowMethods,
                                    AllowOrigins: props.url.cors.allowOrigins,
                                    ExposeHeaders: props.url.cors.exposeHeaders,
                                    MaxAge: props.url.cors.maxAge,
                                }
                                : undefined,
                        })));
                        functionUrl = updateResult.FunctionUrl;
                        // Add public access permission for function URL
                        if (props.url.authType === "NONE") {
                            try {
                                await retry(() => client.send(new AddPermissionCommand({
                                    FunctionName: functionName,
                                    StatementId: "FunctionURLAllowPublicAccess",
                                    Action: "lambda:InvokeFunctionUrl",
                                    Principal: "*",
                                    FunctionUrlAuthType: "NONE",
                                })));
                            }
                            catch (permError) {
                                if (!permError.message?.includes("already exists")) {
                                    logger.warn("Error adding URL permission:", permError);
                                }
                            }
                        }
                    }
                    else {
                        // Create URL configuration if it doesn't exist
                        const createResult = await retry(() => client.send(new CreateFunctionUrlConfigCommand({
                            FunctionName: functionName,
                            AuthType: props.url.authType || "NONE",
                            InvokeMode: props.url.invokeMode || "BUFFERED",
                            Cors: props.url.cors
                                ? {
                                    AllowCredentials: props.url.cors.allowCredentials,
                                    AllowHeaders: props.url.cors.allowHeaders,
                                    AllowMethods: props.url.cors.allowMethods,
                                    AllowOrigins: props.url.cors.allowOrigins,
                                    ExposeHeaders: props.url.cors.exposeHeaders,
                                    MaxAge: props.url.cors.maxAge,
                                }
                                : undefined,
                        })));
                        functionUrl = createResult.FunctionUrl;
                        // Add public access permission for function URL
                        if (props.url.authType === "NONE") {
                            try {
                                await retry(() => client.send(new AddPermissionCommand({
                                    FunctionName: functionName,
                                    StatementId: "FunctionURLAllowPublicAccess",
                                    Action: "lambda:InvokeFunctionUrl",
                                    Principal: "*",
                                    FunctionUrlAuthType: "NONE",
                                })));
                            }
                            catch (permError) {
                                if (!permError.message?.includes("already exists")) {
                                    logger.warn("Error adding URL permission:", permError);
                                }
                            }
                        }
                    }
                }
                catch (error) {
                    if (error.name === "ResourceNotFoundException") {
                        // Create URL configuration if it doesn't exist
                        const createResult = await retry(() => client.send(new CreateFunctionUrlConfigCommand({
                            FunctionName: functionName,
                            AuthType: props.url.authType || "NONE",
                            InvokeMode: props.url.invokeMode || "BUFFERED",
                            Cors: props.url.cors
                                ? {
                                    AllowCredentials: props.url.cors.allowCredentials,
                                    AllowHeaders: props.url.cors.allowHeaders,
                                    AllowMethods: props.url.cors.allowMethods,
                                    AllowOrigins: props.url.cors.allowOrigins,
                                    ExposeHeaders: props.url.cors.exposeHeaders,
                                    MaxAge: props.url.cors.maxAge,
                                }
                                : undefined,
                        })));
                        functionUrl = createResult.FunctionUrl;
                        // Add public access permission for function URL
                        if (props.url.authType === "NONE") {
                            try {
                                await retry(() => client.send(new AddPermissionCommand({
                                    FunctionName: functionName,
                                    StatementId: "FunctionURLAllowPublicAccess",
                                    Action: "lambda:InvokeFunctionUrl",
                                    Principal: "*",
                                    FunctionUrlAuthType: "NONE",
                                })));
                            }
                            catch (permError) {
                                if (!permError.message?.includes("already exists")) {
                                    logger.warn("Error adding URL permission:", permError);
                                }
                            }
                        }
                    }
                    else {
                        throw error;
                    }
                }
            }
            else if (this.output?.url) {
                // Remove URL config if it was previously set but not in current props
                try {
                    await retry(() => client.send(new DeleteFunctionUrlConfigCommand({
                        FunctionName: functionName,
                    })));
                    functionUrl = undefined;
                }
                catch (error) {
                    if (error.name !== "ResourceNotFoundException") {
                        logger.warn("Failed to delete function URL:", error);
                    }
                }
            }
        }
    }
    catch (error) {
        if (error.name === "ResourceNotFoundException") {
            // Create function if it doesn't exist
            const startTime = Date.now();
            let delay = 100; // Start with 100ms delay
            while (true) {
                try {
                    await retry(() => client.send(new CreateFunctionCommand({
                        FunctionName: functionName,
                        Code: { ZipFile: code },
                        Handler: props.handler || "index.handler",
                        Runtime: props.runtime || Runtime.nodejs20x,
                        Role: props.roleArn,
                        Description: props.description,
                        Timeout: props.timeout || 3,
                        MemorySize: props.memorySize || 128,
                        Environment: props.environment
                            ? { Variables: props.environment }
                            : undefined,
                        Architectures: props.architecture
                            ? [props.architecture]
                            : [Architecture.x86_64],
                        Tags: props.tags,
                        Layers: props.layers,
                    })));
                    break; // Success - exit retry loop
                }
                catch (createError) {
                    if (createError.name !== "InvalidParameterValueException" ||
                        !createError.message?.includes("cannot be assumed by Lambda")) {
                        throw createError; // Different error - rethrow
                    }
                    if (Date.now() - startTime > 10000) {
                        throw new Error("Timeout waiting for IAM role to be assumable by Lambda after 10s");
                    }
                    await new Promise((resolve) => setTimeout(resolve, delay));
                    delay = Math.min(delay * 2, 1000); // Exponential backoff capped at 1s
                }
            }
            // Wait for function to be active
            let isCreating = true;
            while (isCreating) {
                const config = await retry(() => client.send(new GetFunctionConfigurationCommand({
                    FunctionName: functionName,
                })));
                isCreating = config.State === "Pending";
                if (isCreating) {
                    await new Promise((resolve) => setTimeout(resolve, 1000));
                }
            }
            // Additional stabilization check - ensure role can be used
            // by waiting a bit more after function becomes Active
            await new Promise((resolve) => setTimeout(resolve, 2000));
            // Verify function is fully stable and role is usable
            await waitForFunctionStabilization(client, functionName);
            // Create URL configuration if needed
            if (props.url) {
                try {
                    const createResult = await retry(() => client.send(new CreateFunctionUrlConfigCommand({
                        FunctionName: functionName,
                        AuthType: props.url.authType || "NONE",
                        InvokeMode: props.url.invokeMode || "BUFFERED",
                        Cors: props.url.cors
                            ? {
                                AllowCredentials: props.url.cors.allowCredentials,
                                AllowHeaders: props.url.cors.allowHeaders,
                                AllowMethods: props.url.cors.allowMethods,
                                AllowOrigins: props.url.cors.allowOrigins,
                                ExposeHeaders: props.url.cors.exposeHeaders,
                                MaxAge: props.url.cors.maxAge,
                            }
                            : undefined,
                    })));
                    functionUrl = createResult.FunctionUrl;
                    // Add public access permission for function URL
                    if (props.url.authType === "NONE") {
                        try {
                            await retry(() => client.send(new AddPermissionCommand({
                                FunctionName: functionName,
                                StatementId: "FunctionURLAllowPublicAccess",
                                Action: "lambda:InvokeFunctionUrl",
                                Principal: "*",
                                FunctionUrlAuthType: "NONE",
                            })));
                        }
                        catch (permError) {
                            if (!permError.message?.includes("already exists")) {
                                logger.warn("Error adding URL permission:", permError);
                            }
                        }
                    }
                }
                catch (error) {
                    logger.warn("Failed to create function URL:", error);
                }
            }
        }
        else {
            throw error;
        }
    }
    // Get complete function details
    const [_func, config] = await Promise.all([
        retry(() => client.send(new GetFunctionCommand({
            FunctionName: functionName,
        }))),
        retry(() => client.send(new GetFunctionConfigurationCommand({
            FunctionName: functionName,
        }))),
    ]);
    // Try to get function URL if it wasn't already retrieved and URL is configured
    if (!functionUrl && (props.url || this.output?.url)) {
        try {
            const urlConfig = await retry(() => client.send(new GetFunctionUrlConfigCommand({
                FunctionName: functionName,
            })));
            functionUrl = urlConfig.FunctionUrl;
        }
        catch (error) {
            if (error.name !== "ResourceNotFoundException") {
                logger.warn("Failed to get function URL:", error);
            }
        }
    }
    return {
        ...props,
        arn: config.FunctionArn,
        functionName,
        lastModified: config.LastModified,
        version: config.Version,
        qualifiedArn: `${config.FunctionArn}:${config.Version}`,
        invokeArn: `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${config.FunctionArn}/invocations`,
        sourceCodeHash: config.CodeSha256,
        sourceCodeSize: config.CodeSize,
        ephemeralStorageSize: config.EphemeralStorage?.Size,
        architectures: config.Architectures || [],
        masterArn: config.MasterArn,
        revisionId: config.RevisionId,
        state: config.State,
        stateReason: config.StateReason,
        stateReasonCode: config.StateReasonCode,
        lastUpdateStatus: config.LastUpdateStatus,
        lastUpdateStatusReason: config.LastUpdateStatusReason,
        lastUpdateStatusReasonCode: config.LastUpdateStatusReasonCode,
        packageType: config.PackageType,
        signingProfileVersionArn: config.SigningProfileVersionArn,
        signingJobArn: config.SigningJobArn,
        functionUrl: functionUrl,
    };
});
// Helper to wait for function to stabilize
async function waitForFunctionStabilization(client, functionName) {
    const { GetFunctionConfigurationCommand } = await import("@aws-sdk/client-lambda");
    while (true) {
        const config = await retry(() => client.send(new GetFunctionConfigurationCommand({
            FunctionName: functionName,
        })));
        // Check if function is in a stable state
        if (config.State === "Active" && config.LastUpdateStatus === "Successful") {
            break;
        }
        // If there's a failure, throw an error
        if (config.State === "Failed" || config.LastUpdateStatus === "Failed") {
            throw new Error(`Function failed to stabilize: ${config.StateReason || config.LastUpdateStatusReason}`);
        }
        await new Promise((resolve) => setTimeout(resolve, 1000));
    }
}
const handlerRegex = /^(.*)\.([A-Za-z0-9_]+)$/;
function parseFile(handler) {
    const match = handler.match(handlerRegex);
    if (!match) {
        throw new Error(`Invalid handler: ${handler}`);
    }
    return match[1];
}
// Helper to zip the code
async function zipCode(props) {
    const fileContent = props.bundle.content;
    const fileName = parseFile(props.handler) +
        (props.bundle.format === "cjs" ? ".cjs" : ".mjs");
    // Create a zip buffer in memory
    const zip = new (await import("jszip")).default();
    zip.file(fileName, fileContent);
    return zip.generateAsync({
        type: "nodebuffer",
        compression: "DEFLATE",
        platform: "UNIX",
    });
}
async function resolveRegion(client) {
    const region = client.config.region;
    if (typeof region === "string")
        return region;
    if (typeof region === "function")
        return region();
    throw new Error("Could not resolve AWS region");
}
//# sourceMappingURL=function.js.map