'use strict';

const getCircularReplacer = () => {
    const seen = new WeakSet();
    return (key, value) => {
        if (typeof value === "object" && value !== null) {
            if (seen.has(value)) {
                return "[Circular]";
            }
            seen.add(value);
        }
        return value;
    };
};

const sleep = (seconds) => {
    return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
};

const waiterServiceDefaults = {
    minDelay: 2,
    maxDelay: 120,
};
exports.WaiterState = void 0;
(function (WaiterState) {
    WaiterState["ABORTED"] = "ABORTED";
    WaiterState["FAILURE"] = "FAILURE";
    WaiterState["SUCCESS"] = "SUCCESS";
    WaiterState["RETRY"] = "RETRY";
    WaiterState["TIMEOUT"] = "TIMEOUT";
})(exports.WaiterState || (exports.WaiterState = {}));
const checkExceptions = (result) => {
    if (result.state === exports.WaiterState.ABORTED) {
        const abortError = new Error(`${JSON.stringify({
            ...result,
            reason: "Request was aborted",
        }, getCircularReplacer())}`);
        abortError.name = "AbortError";
        throw abortError;
    }
    else if (result.state === exports.WaiterState.TIMEOUT) {
        const timeoutError = new Error(`${JSON.stringify({
            ...result,
            reason: "Waiter has timed out",
        }, getCircularReplacer())}`);
        timeoutError.name = "TimeoutError";
        throw timeoutError;
    }
    else if (result.state !== exports.WaiterState.SUCCESS) {
        throw new Error(`${JSON.stringify(result, getCircularReplacer())}`);
    }
    return result;
};

const runPolling = async ({ minDelay, maxDelay, maxWaitTime, abortController, client, abortSignal }, input, acceptorChecks) => {
    const observedResponses = {};
    const [minDelayMs, maxDelayMs] = [minDelay * 1000, maxDelay * 1000];
    let currentAttempt = 0;
    const waitUntil = Date.now() + maxWaitTime * 1000;
    const warn403Time = Date.now() + 60_000;
    let didWarn403 = false;
    while (true) {
        if (currentAttempt > 0) {
            const delayMs = exponentialBackoffWithJitter(minDelayMs, maxDelayMs, currentAttempt, waitUntil);
            if (abortController?.signal?.aborted || abortSignal?.aborted) {
                const message = "AbortController signal aborted.";
                observedResponses[message] |= 0;
                observedResponses[message] += 1;
                return { state: exports.WaiterState.ABORTED, observedResponses };
            }
            if (Date.now() + delayMs > waitUntil) {
                return { state: exports.WaiterState.TIMEOUT, observedResponses };
            }
            await sleep(delayMs / 1_000);
        }
        const { state, reason } = await acceptorChecks(client, input);
        if (reason) {
            const message = createMessageFromResponse(reason);
            observedResponses[message] |= 0;
            observedResponses[message] += 1;
        }
        if (state !== exports.WaiterState.RETRY) {
            return { state, reason, final: reason, observedResponses };
        }
        currentAttempt += 1;
        if (!didWarn403 && Date.now() >= warn403Time) {
            checkWarn403(observedResponses, client);
            didWarn403 = true;
        }
    }
};
const checkWarn403 = (observedResponses = {}, client) => {
    const orderedErrors = Object.keys(observedResponses);
    let count403 = 0;
    for (const response of orderedErrors) {
        const n = observedResponses[response] | 0;
        if (response.startsWith("403:")) {
            count403 += n;
        }
    }
    const clientLogger = client?.config?.logger;
    const warningLogger = typeof clientLogger?.warn === "function" && !clientLogger.constructor?.name?.includes?.("NoOpLogger")
        ? clientLogger
        : console;
    if (count403 >= 3 || orderedErrors[orderedErrors.length - 1].startsWith("403:")) {
        warningLogger.warn(`@smithy/util-waiter WARN - 403 status code encountered during waiter polling.`);
    }
};
const createMessageFromResponse = (reason) => {
    const status = reason?.$response?.statusCode ?? reason?.$metadata?.httpStatusCode;
    if (reason?.$responseBodyText) {
        return `${status ? status + ": " : ""}Deserialization error for body: ${reason.$responseBodyText}`;
    }
    if (status) {
        if (reason?.$response || reason?.message) {
            return `${status ?? "Unknown"}: ${reason?.message}`;
        }
        return `${status}: OK`;
    }
    return String(reason?.message ?? JSON.stringify(reason, getCircularReplacer()) ?? "Unknown");
};
const exponentialBackoffWithJitter = (minDelayMs, maxDelayMs, attempt, waitUntil) => {
    const attemptCountCeiling = Math.log(maxDelayMs / minDelayMs) / Math.log(2) + 1;
    if (attempt > attemptCountCeiling) {
        return maxDelayMs;
    }
    const delay = minDelayMs * 2 ** (attempt - 1);
    const capped = Math.min(delay, maxDelayMs);
    const waitFor = randomInRange(minDelayMs, capped);
    if (Date.now() + waitFor > waitUntil) {
        const timeRemaining = waitUntil - Date.now();
        return Math.max(0, timeRemaining - 500);
    }
    return waitFor;
};
const randomInRange = (min, max) => min + Math.random() * (max - min);

const validateWaiterOptions = (options) => {
    if (options.maxWaitTime <= 0) {
        throw new Error(`WaiterConfiguration.maxWaitTime must be greater than 0`);
    }
    else if (options.minDelay <= 0) {
        throw new Error(`WaiterConfiguration.minDelay must be greater than 0`);
    }
    else if (options.maxDelay <= 0) {
        throw new Error(`WaiterConfiguration.maxDelay must be greater than 0`);
    }
    else if (options.maxWaitTime <= options.minDelay) {
        throw new Error(`WaiterConfiguration.maxWaitTime [${options.maxWaitTime}] must be greater than WaiterConfiguration.minDelay [${options.minDelay}] for this waiter`);
    }
    else if (options.maxDelay < options.minDelay) {
        throw new Error(`WaiterConfiguration.maxDelay [${options.maxDelay}] must be greater than WaiterConfiguration.minDelay [${options.minDelay}] for this waiter`);
    }
};

const abortTimeout = (abortSignal) => {
    let onAbort;
    const promise = new Promise((resolve) => {
        onAbort = () => resolve({ state: exports.WaiterState.ABORTED });
        if (typeof abortSignal.addEventListener === "function") {
            abortSignal.addEventListener("abort", onAbort);
        }
        else {
            abortSignal.onabort = onAbort;
        }
    });
    return {
        clearListener() {
            if (typeof abortSignal.removeEventListener === "function") {
                abortSignal.removeEventListener("abort", onAbort);
            }
        },
        aborted: promise,
    };
};
const createWaiter = async (options, input, acceptorChecks) => {
    const params = {
        ...waiterServiceDefaults,
        ...options,
    };
    validateWaiterOptions(params);
    const exitConditions = [runPolling(params, input, acceptorChecks)];
    const finalize = [];
    if (options.abortSignal) {
        const { aborted, clearListener } = abortTimeout(options.abortSignal);
        finalize.push(clearListener);
        exitConditions.push(aborted);
    }
    if (options.abortController?.signal) {
        const { aborted, clearListener } = abortTimeout(options.abortController.signal);
        finalize.push(clearListener);
        exitConditions.push(aborted);
    }
    return Promise.race(exitConditions).then((result) => {
        for (const fn of finalize) {
            fn();
        }
        return result;
    });
};

exports.checkExceptions = checkExceptions;
exports.createWaiter = createWaiter;
exports.waiterServiceDefaults = waiterServiceDefaults;
