"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractRateLimitBackoff = extractRateLimitBackoff;
function extractRateLimitBackoff(error, now = new Date()) {
    return visitError(error, now, new Set());
}
function visitError(error, now, seen) {
    if (error === null || error === undefined || seen.has(error)) {
        return null;
    }
    if (typeof error !== "object" && !(error instanceof Error)) {
        return null;
    }
    seen.add(error);
    const candidate = extractFromErrorObject(error, now);
    if (candidate) {
        return candidate;
    }
    if (error instanceof Error) {
        return visitError(error.cause, now, seen);
    }
    if ("cause" in error) {
        return visitError(error.cause, now, seen);
    }
    return null;
}
function extractFromErrorObject(error, now) {
    const status = readNumericProperty(error, "status");
    const headers = readHeaders(error);
    const retryAfter = parseRetryAfter(readHeader(headers, "retry-after"), now);
    const resetAt = parseResetAt(readHeader(headers, "x-ratelimit-reset"));
    const remaining = readHeader(headers, "x-ratelimit-remaining");
    const message = readText(error);
    const textLooksLikeRateLimit = /rate limit|retry-after|retry after|too many requests|abuse/i.test(message);
    const looksLikeRateLimit = status === 429 ||
        (status === 403 &&
            (retryAfter !== null ||
                resetAt !== null ||
                remaining === "0" ||
                textLooksLikeRateLimit)) ||
        (status === null &&
            (retryAfter !== null ||
                resetAt !== null ||
                remaining === "0" ||
                textLooksLikeRateLimit)) ||
        retryAfter !== null ||
        resetAt !== null ||
        remaining === "0" ||
        textLooksLikeRateLimit;
    if (!looksLikeRateLimit) {
        return null;
    }
    if (retryAfter) {
        return {
            nextRetryAt: retryAfter,
            reason: "retry-after",
        };
    }
    if (resetAt) {
        return {
            nextRetryAt: resetAt,
            reason: "x-ratelimit-reset",
        };
    }
    if (remaining === "0") {
        const messageBackoff = parseRetryAfterFromText(message, now);
        if (messageBackoff) {
            return messageBackoff;
        }
    }
    return (parseRetryAfterFromText(message, now) ?? {
        nextRetryAt: new Date(now.getTime() + 60_000).toISOString(),
        reason: "rate-limit-default",
    });
}
function readNumericProperty(error, key) {
    const value = Reflect.get(error, key);
    const parsed = typeof value === "number"
        ? value
        : typeof value === "string"
            ? Number(value)
            : Number.NaN;
    return Number.isFinite(parsed) ? parsed : null;
}
function readHeaders(error) {
    const response = readObjectProperty(error, "response");
    if (response) {
        const headers = readHeadersObject(response, "headers");
        if (headers !== null) {
            return headers;
        }
    }
    return readHeadersObject(error, "headers");
}
function readObjectProperty(value, key) {
    const candidate = Reflect.get(value, key);
    if (!candidate || typeof candidate !== "object") {
        return null;
    }
    return candidate;
}
function readHeader(headers, name) {
    if (!headers) {
        return null;
    }
    if (typeof headers.get === "function") {
        const value = headers.get(name);
        return value?.trim() || null;
    }
    const normalizedName = name.toLowerCase();
    for (const [key, value] of Object.entries(headers)) {
        if (key.toLowerCase() !== normalizedName) {
            continue;
        }
        if (Array.isArray(value)) {
            const first = value[0];
            return typeof first === "string" ? first.trim() || null : null;
        }
        if (typeof value === "string") {
            return value.trim() || null;
        }
        if (typeof value === "number") {
            return String(value);
        }
    }
    return null;
}
function readHeadersObject(value, key) {
    const candidate = readObjectProperty(value, key);
    if (candidate === null) {
        return null;
    }
    if (isHeaders(candidate)) {
        return candidate;
    }
    return candidate;
}
function isHeaders(value) {
    return typeof Reflect.get(value, "get") === "function";
}
function readText(error) {
    const message = readStringProperty(error, "message");
    const response = readObjectProperty(error, "response");
    const responseMessage = response
        ? readStringProperty(response, "message")
        : null;
    return [message, responseMessage].filter(Boolean).join("\n");
}
function readStringProperty(value, key) {
    const candidate = Reflect.get(value, key);
    return typeof candidate === "string" ? candidate : null;
}
function parseRetryAfter(rawValue, now) {
    if (!rawValue) {
        return null;
    }
    const trimmed = rawValue.trim();
    if (!trimmed) {
        return null;
    }
    if (/^\d+$/u.test(trimmed)) {
        return new Date(now.getTime() + Number.parseInt(trimmed, 10) * 1_000).toISOString();
    }
    const parsed = Date.parse(trimmed);
    return Number.isNaN(parsed) ? null : new Date(parsed).toISOString();
}
function parseResetAt(rawValue) {
    if (!rawValue) {
        return null;
    }
    const trimmed = rawValue.trim();
    if (!trimmed) {
        return null;
    }
    if (/^\d+$/.test(trimmed)) {
        const rawNumber = Number.parseInt(trimmed, 10);
        const epochMs = trimmed.length >= 13 ? rawNumber : rawNumber * 1_000;
        return new Date(epochMs).toISOString();
    }
    const parsed = Date.parse(trimmed);
    return Number.isNaN(parsed) ? null : new Date(parsed).toISOString();
}
function parseRetryAfterFromText(text, now) {
    const normalized = text.toLowerCase();
    if (!normalized.includes("rate limit") &&
        !normalized.includes("retry-after") &&
        !normalized.includes("retry after") &&
        !normalized.includes("too many requests") &&
        !normalized.includes("abuse")) {
        return null;
    }
    const retryAfterMatch = text.match(/retry[- ]after[: ]+(\d+)(?:\s*(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h))?/i);
    if (retryAfterMatch?.[1]) {
        const amount = Number.parseInt(retryAfterMatch[1], 10);
        const unit = retryAfterMatch[2]?.toLowerCase() ?? "s";
        const multiplier = unit.startsWith("h")
            ? 60 * 60 * 1_000
            : unit.startsWith("m") && unit !== "ms"
                ? 60 * 1_000
                : 1_000;
        return {
            nextRetryAt: new Date(now.getTime() + amount * multiplier).toISOString(),
            reason: "retry-after-text",
        };
    }
    const resetMatch = text.match(/\b(?:reset|retry[- ]after)\b[^0-9]*(\d{10,13})\b/i);
    if (resetMatch?.[1]) {
        const rawValue = Number.parseInt(resetMatch[1], 10);
        const epochMs = resetMatch[1].length === 13 ? rawValue : rawValue * 1_000;
        return {
            nextRetryAt: new Date(epochMs).toISOString(),
            reason: "reset-text",
        };
    }
    return null;
}
