/**
 * Polling Utilities
 *
 * This module provides utilities for polling HTTP endpoints until completion,
 * with configurable retry logic and exponential backoff.
 */
import { ZapierTimeoutError, ZapierApiError, ZapierValidationError, } from "../types/errors";
import { sleep, calculateErrorBackoffMs, MAX_CONSECUTIVE_ERRORS, } from "../utils/retry-utils";
import { isAbortError } from "../utils/abort-utils";
// Constants
const DEFAULT_TIMEOUT_MS = 180000;
const DEFAULT_SUCCESS_STATUS = 200;
const DEFAULT_PENDING_STATUS = 202;
const DEFAULT_INITIAL_DELAY_MS = 50;
const DEFAULT_MAX_POLLING_INTERVAL_MS = 60000;
// Polling stages: [elapsed_time_threshold_ms, poll_interval_ms]
const POLLING_STAGES = [
    [125, 125], // Up to 125ms: poll every 125ms
    [375, 250], // Up to 375ms: poll every 250ms
    [875, 500], // Up to 875ms: poll every 500ms
    [10000, 1000], // Up to 10s: poll every 1s
    [30000, 2500], // Up to 30s: poll every 2.5s
    [60000, 5000], // Up to 60s: poll every 5s
    [180000, 10000], // Up to 3min: poll every 10s
    // Beyond 3min: use DEFAULT_MAX_POLLING_INTERVAL_MS (60s)
];
/**
 * Get the appropriate polling interval based on elapsed time.
 * Starts with frequent polling and backs off as time passes.
 * @param elapsedMs Time elapsed since polling started (in milliseconds)
 * @returns The polling interval to use (in milliseconds)
 */
export function getPollingInterval(elapsedMs) {
    const stage = POLLING_STAGES.find(([threshold]) => elapsedMs < threshold);
    return stage ? stage[1] : DEFAULT_MAX_POLLING_INTERVAL_MS;
}
function makeAbortError() {
    if (typeof DOMException !== "undefined") {
        return new DOMException("Polling was aborted", "AbortError");
    }
    const err = new Error("Polling was aborted");
    err.name = "AbortError";
    return err;
}
const processResponse = async (response, successStatus, pendingStatus, isPending, resultExtractor, errorCount) => {
    // Handle other error responses
    if (!response.ok) {
        return {
            status: "continue" /* PollStatus.Continue */,
            // If for some reason the status is pending, we don't want to increment the error count
            errorCount: response.status === pendingStatus ? errorCount : errorCount + 1,
        };
    }
    // Check for successful completion
    if (response.status === successStatus) {
        try {
            const resultJson = await response.json();
            // Check if the response body indicates we should continue polling
            if (isPending && isPending(resultJson)) {
                return {
                    status: "continue" /* PollStatus.Continue */,
                    errorCount: 0,
                };
            }
            return {
                result: resultExtractor(resultJson),
                status: "success" /* PollStatus.Success */,
                errorCount: 0,
            };
        }
        catch (error) {
            throw new ZapierApiError("Result extractor failed to parse successful response as JSON", {
                statusCode: response.status,
                cause: error,
            });
        }
    }
    // If it's not pending, it's unexpected
    if (response.status !== pendingStatus) {
        throw new ZapierApiError(`Unexpected response status during polling: ${response.status}`, {
            statusCode: response.status,
        });
    }
    // It's still pending, so we continue polling
    return {
        status: "continue" /* PollStatus.Continue */,
        errorCount: 0,
    };
};
/**
 * Polls an endpoint until completion, timeout, or error
 * @param options Configuration options for polling
 * @returns The extracted result from the successful response
 * @throws {ZapierValidationError} When the input parameters are invalid
 * @throws {ZapierTimeoutError} When the operation times out
 * @throws {ZapierApiError} When the API returns consecutive errors
 */
export async function pollUntilComplete(options) {
    const { fetchPoll, timeoutMs = DEFAULT_TIMEOUT_MS, initialDelay = DEFAULT_INITIAL_DELAY_MS, successStatus = DEFAULT_SUCCESS_STATUS, pendingStatus = DEFAULT_PENDING_STATUS, isPending, resultExtractor = (response) => response, signal, } = options;
    // Validate input parameters
    if (timeoutMs <= 0) {
        throw new ZapierValidationError("Timeout must be greater than 0", {
            details: { timeoutMs },
        });
    }
    if (initialDelay < 0) {
        throw new ZapierValidationError("Initial delay must be non-negative", {
            details: { initialDelay },
        });
    }
    if (signal?.aborted)
        throw makeAbortError();
    const startTime = Date.now();
    let attempts = 0;
    let errorCount = 0;
    // Apply initial delay if specified. `sleep(..., signal)` resolves
    // early on abort; the explicit re-check below converts that into a
    // proper AbortError throw so the loop doesn't issue a request after
    // the caller cancelled.
    if (initialDelay > 0) {
        await sleep(initialDelay, signal);
        if (signal?.aborted)
            throw makeAbortError();
    }
    while (true) {
        if (signal?.aborted)
            throw makeAbortError();
        const elapsedTime = Date.now() - startTime;
        // Check if we've exceeded the timeout
        if (elapsedTime >= timeoutMs) {
            throw new ZapierTimeoutError(`Operation timed out after ${Math.floor(elapsedTime / 1000)}s (${attempts} attempts)`, {
                attempts,
            });
        }
        // Wait before polling (except on first attempt)
        if (attempts > 0) {
            const interval = getPollingInterval(elapsedTime);
            const waitTime = calculateErrorBackoffMs(interval, errorCount);
            await sleep(waitTime, signal);
            if (signal?.aborted)
                throw makeAbortError();
        }
        attempts++;
        // Perform the poll request
        try {
            const response = await fetchPoll();
            const { result, errorCount: newErrorCount, status, } = await processResponse(response, successStatus, pendingStatus, isPending, resultExtractor, errorCount);
            errorCount = newErrorCount;
            if (status === "success" /* PollStatus.Success */) {
                return result;
            }
            if (errorCount >= MAX_CONSECUTIVE_ERRORS) {
                // Too many consecutive errors, fail
                throw new ZapierApiError(`Poll request failed: ${response.status} ${response.statusText}`, { statusCode: response.status });
            }
        }
        catch (error) {
            // Abort is intent, not failure. Surface immediately rather than
            // counting it toward MAX_CONSECUTIVE_ERRORS and wrapping in
            // ZapierApiError.
            if (isAbortError(error))
                throw error;
            errorCount++;
            if (errorCount >= MAX_CONSECUTIVE_ERRORS) {
                throw new ZapierApiError(`Failed to poll after ${errorCount} consecutive errors: ${error instanceof Error ? error.message : String(error)}`, {
                    cause: error,
                });
            }
        }
        // Continue polling if status is pending
    }
}
