import { fromNodeProviderChain } from "@aws-sdk/credential-providers";
import { loadConfig } from "@smithy/node-config-provider";
import { AwsClient } from "aws4fetch";
import { logger } from "../../util/logger.js";
import { safeFetch } from "../../util/safe-fetch.js";
import { AlreadyExistsError, CloudControlError, ConcurrentOperationError, NetworkError, NotFoundError, RequestError, ResourceNotFoundException, ThrottlingException, UpdateFailedError, ValidationException, } from "./error.js";
const getRegion = loadConfig({
    environmentVariableSelector: (env) => env.AWS_REGION || env.AWS_DEFAULT_REGION,
    configFileSelector: (profile) => profile.region,
    default: undefined,
});
/**
 * Create a Cloud Control API client
 */
export async function createCloudControlClient(options = {}) {
    const credentials = await fromNodeProviderChain()();
    const region = options.region ?? // 1. favor explicit region passed in
        (await getRegion()) ?? // 2-4. resolve from AWS profile, loadConfig, and environment
        process.env.AWS_REGION ??
        process.env.AWS_DEFAULT_REGION;
    if (!region) {
        throw new Error("No region found. Please set AWS_REGION or AWS_DEFAULT_REGION in the environment or in your AWS profile.");
    }
    const client = new AwsClient({
        ...credentials,
        service: "cloudcontrolapi",
        region,
    });
    return new CloudControlClient(client, {
        ...options,
        region,
    });
}
export class CloudControlClient {
    client;
    region;
    initialPollingDelay;
    maxPollingDelay;
    maxRetries;
    constructor(client, options) {
        this.client = client;
        this.region = options.region || "us-east-1";
        this.initialPollingDelay = options.initialPollingDelay || 1000;
        this.maxPollingDelay = options.maxPollingDelay || 10000;
        this.maxRetries = options.maxRetries || 3;
    }
    /**
     * Create a new resource
     */
    async createResource(typeName, desiredState) {
        return this.sync("CreateResource", {
            TypeName: typeName,
            DesiredState: JSON.stringify(desiredState),
        });
    }
    /**
     * Get a resource's current state
     */
    async getResource(typeName, identifier) {
        try {
            const resource = await this.fetch("GetResource", {
                TypeName: typeName,
                Identifier: identifier,
            });
            return JSON.parse(resource.ResourceDescription.Properties);
        }
        catch (error) {
            if (error instanceof ResourceNotFoundException) {
                return undefined;
            }
            throw error;
        }
    }
    /**
     * Update a resource
     */
    async updateResource(typeName, identifier, patchDocument) {
        return this.sync("UpdateResource", {
            TypeName: typeName,
            Identifier: identifier,
            PatchDocument: JSON.stringify(patchDocument),
        });
    }
    /**
     * Delete a resource
     */
    async deleteResource(typeName, identifier) {
        return this.sync("DeleteResource", {
            TypeName: typeName,
            Identifier: identifier,
        });
    }
    /**
     * List resources of a given type
     */
    listResources(typeName, nextToken) {
        return this.fetch("ListResources", {
            TypeName: typeName,
            NextToken: nextToken,
        });
    }
    /**
     * Poll an operation until it completes
     */
    async poll(requestToken, action) {
        let delay = this.initialPollingDelay;
        let logged = false;
        while (true) {
            try {
                const response = await this.fetch("GetResourceRequestStatus", {
                    RequestToken: requestToken,
                });
                if (response.ProgressEvent.OperationStatus === "SUCCESS") {
                    return response.ProgressEvent;
                }
                if (response.ProgressEvent.OperationStatus === "FAILED") {
                    const errorCode = response.ProgressEvent.ErrorCode;
                    const progressEvent = response.ProgressEvent;
                    if (errorCode === "AlreadyExists") {
                        throw new AlreadyExistsError(progressEvent);
                    }
                    else if (errorCode === "NotFound") {
                        throw new NotFoundError(progressEvent);
                    }
                    throw new UpdateFailedError(progressEvent);
                }
                // Use the suggested retry delay if provided
                const waitTime = response.ProgressEvent.RetryAfter
                    ? Math.max(100, response.ProgressEvent.RetryAfter * 1000 - Date.now())
                    : delay;
                if (!logged) {
                    logged = true;
                    logger.log(`Polling for ${response.ProgressEvent.Identifier} (${response.ProgressEvent.TypeName}) to stabilize`);
                }
                await new Promise((resolve) => setTimeout(resolve, waitTime));
                delay = Math.min(delay * 2, this.maxPollingDelay);
            }
            catch (error) {
                if (error instanceof NotFoundError && action === "DeleteResource") {
                    return error.progressEvent;
                }
                else if (error instanceof CloudControlError) {
                    throw error;
                }
                else if (error instanceof ValidationException) {
                    throw error;
                }
                // For retryable errors, continue polling
                await new Promise((resolve) => setTimeout(resolve, Math.min(delay * 2, this.maxPollingDelay)));
                delay = Math.min(delay * 2, this.maxPollingDelay);
            }
        }
    }
    async sync(action, props) {
        const { ProgressEvent } = await this.fetch(action, props);
        return this.poll(ProgressEvent.RequestToken, action);
    }
    async fetch(action, params, options) {
        let attempt = 0;
        const maxRetries = options?.maxRetries || this.maxRetries;
        while (true) {
            try {
                const args = [
                    `https://cloudcontrolapi.${this.region}.amazonaws.com/?Action=${action}&Version=2021-09-30`,
                    {
                        method: "POST",
                        headers: {
                            "content-type": "application/x-amz-json-1.0",
                            "x-amz-target": `CloudApiService.${action}`,
                        },
                        body: JSON.stringify(params),
                    },
                ];
                const signedRequest = await this.client.sign(...args);
                const response = await safeFetch(signedRequest);
                if (!response.ok) {
                    const data = await response.json();
                    if (data.__type ===
                        "com.amazon.cloudapiservice#ResourceNotFoundException") {
                        throw new ResourceNotFoundException(response);
                    }
                    if (data.__type ===
                        "com.amazon.cloudapiservice#ConcurrentOperationException") {
                        // Extract request token from message
                        const message = data.Message || "";
                        const requestTokenMatch = message.match(/RequestToken\s+([a-fA-F0-9-]+)/);
                        if (!requestTokenMatch) {
                            throw new RequestError(response, {
                                ...data,
                                message: `ConcurrentOperationException without request token: ${message}`,
                            });
                        }
                        throw new ConcurrentOperationError(message, requestTokenMatch[1]);
                    }
                    if (data.__type === "com.amazon.coral.availability#ThrottlingException") {
                        throw new ThrottlingException(response, data);
                    }
                    if (data.__type === "com.amazon.coral.validate#ValidationException") {
                        throw new ValidationException(response, data);
                    }
                    throw new RequestError(response, data);
                }
                return (await response.json());
            }
            catch (error) {
                // Handle other CloudControl errors (don't retry, except ThrottlingException)
                if (error instanceof CloudControlError &&
                    !(error instanceof ThrottlingException)) {
                    throw error;
                }
                // Handle retryable errors (throttling and network errors)
                if (attempt < maxRetries) {
                    // Use exponential backoff with jitter, capped at 3 seconds
                    const baseDelay = Math.min(2 ** attempt * 1000, 3000);
                    const jitter = Math.random() * 0.1 * baseDelay;
                    const retryDelay = baseDelay + jitter;
                    const errorType = error instanceof ThrottlingException
                        ? "Throttling"
                        : "Network error";
                    logger.log(`${errorType} detected, retrying in ${Math.round(retryDelay)}ms (attempt ${attempt + 1}/${maxRetries + 1})`);
                    await new Promise((resolve) => setTimeout(resolve, retryDelay));
                    attempt++;
                    continue;
                }
                // Max retries exceeded
                if (error instanceof ThrottlingException) {
                    logger.error(`Max retries (${maxRetries}) exceeded for throttling exception`);
                    throw error;
                }
                throw new NetworkError(error.message || "Network error during Cloud Control API request");
            }
        }
    }
}
//# sourceMappingURL=client.js.map