import * as Challenge from '../../Challenge.js';
import * as Expires from '../../Expires.js';
import * as AcceptPayment from '../../internal/AcceptPayment.js';
// We tag wrappers with a global symbol so we can recognize wrappers created by mppx,
// even across multiple module instances/bundles. This lets restore() avoid clobbering
// an unrelated fetch installed by user code or another library.
const MPPX_FETCH_WRAPPER = Symbol.for('mppx.fetch.wrapper');
let originalFetch;
/**
 * Creates a fetch wrapper that automatically handles 402 Payment Required responses.
 *
 * @example
 * ```ts
 * import { Fetch, tempo } from 'mppx/client'
 * import { privateKeyToAccount } from 'viem/accounts'
 *
 * const fetch = Fetch.from({
 *   methods: [
 *     tempo({
 *       account: privateKeyToAccount('0x...'),
 *     }),
 *   ],
 * })
 *
 * // Use the wrapped fetch — handles 402 automatically
 * const res = await fetch('https://api.example.com/resource')
 * ```
 *
 */
export function from(config) {
    const { acceptPayment, acceptPaymentPolicy = 'always', fetch = globalThis.fetch, methods, onChallenge, } = config;
    const resolvedAcceptPayment = acceptPayment ?? AcceptPayment.resolve(methods);
    // Always operate on the true underlying fetch to avoid wrapper-on-wrapper stacking,
    // which can duplicate retries and make restore semantics fragile.
    const baseFetch = unwrapFetch(fetch);
    const wrappedFetch = async (input, init) => {
        const callerHeaders = getCallerHeaders(input, init?.headers);
        const hasExplicitAcceptPayment = callerHeaders.has('Accept-Payment');
        const paymentPreferences = resolvePaymentPreferences(callerHeaders, resolvedAcceptPayment);
        const initialRequest = prepareInitialRequest(input, init, callerHeaders, paymentPreferences.header, hasExplicitAcceptPayment, acceptPaymentPolicy);
        const response = await baseFetch(initialRequest.input, initialRequest.init);
        if (response.status !== 402)
            return response;
        // Only extract context for payment handling after confirming 402.
        const context = init?.context;
        const { context: _, ...fetchInit } = (initialRequest.init ?? {});
        // Parse all challenges from the response (supports merged WWW-Authenticate headers).
        const challenges = Challenge.fromResponseList(response);
        const selected = AcceptPayment.selectChallenge(challenges, methods, paymentPreferences.entries);
        if (!selected)
            throw new Error(`No method found for challenges: ${challenges.map((c) => `${c.method}.${c.intent}`).join(', ')}. Available: ${methods.map((m) => `${m.name}.${m.intent}`).join(', ')}`);
        const { challenge, method: mi } = selected;
        if (challenge.expires)
            Expires.assert(challenge.expires, challenge.id);
        const onChallengeCredential = onChallenge
            ? await onChallenge(challenge, {
                createCredential: async (overrideContext) => resolveCredential(challenge, mi, overrideContext ?? context),
            })
            : undefined;
        const credential = onChallengeCredential ?? (await resolveCredential(challenge, mi, context));
        validateCredentialHeaderValue(credential);
        return baseFetch(initialRequest.input, {
            ...fetchInit,
            headers: withAuthorizationHeader(initialRequest.headers, credential),
        });
    };
    wrappedFetch[MPPX_FETCH_WRAPPER] = baseFetch;
    return wrappedFetch;
}
/**
 * Replaces the global `fetch` with a payment-aware wrapper.
 *
 * @example
 * ```ts
 * import { Fetch, tempo } from 'mppx/client'
 * import { privateKeyToAccount } from 'viem/accounts'
 *
 * Fetch.polyfill({
 *   methods: [
 *     tempo({
 *       account: privateKeyToAccount('0x...'),
 *     }),
 *   ],
 * })
 *
 * // Global fetch now handles 402 automatically
 * const res = await fetch('https://api.example.com/resource')
 * ```
 */
export function polyfill(config) {
    // Defensive guard for runtimes/tests where fetch might be non-configurable.
    const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'fetch');
    if (!descriptor || (!descriptor.writable && !descriptor.set)) {
        throw new Error('globalThis.fetch is not writable');
    }
    if (!originalFetch)
        originalFetch = globalThis.fetch;
    globalThis.fetch = from({
        ...config,
        acceptPaymentPolicy: config.acceptPaymentPolicy ?? (isBrowser() ? 'same-origin' : 'always'),
        fetch: globalThis.fetch,
    });
}
/**
 * Restores the original `fetch` after calling `polyfill`.
 *
 * @example
 * ```ts
 * import { Fetch } from 'mppx/client'
 *
 * Fetch.polyfill({ methods: [...] })
 *
 * // ... use payment-aware fetch ...
 *
 * Fetch.restore()
 * ```
 */
export function restore() {
    // Only restore if the current fetch is still an mppx wrapper.
    // If app code replaced fetch after polyfill(), we must not overwrite it.
    if (originalFetch && isWrappedFetch(globalThis.fetch)) {
        globalThis.fetch = originalFetch;
        originalFetch = undefined;
    }
}
/** @internal Normalizes headers to a plain object for spreading. */
export function normalizeHeaders(headers) {
    if (!headers)
        return {};
    if (headers instanceof Headers) {
        const result = {};
        headers.forEach((value, key) => {
            result[key] = value;
        });
        return result;
    }
    if (Array.isArray(headers))
        return Object.fromEntries(headers);
    return headers;
}
/** @internal */
function withAuthorizationHeader(headers, credential) {
    const normalized = normalizeHeaders(headers);
    // Remove any existing Authorization header regardless of casing to avoid
    // duplicate/conflicting credentials on retry.
    for (const key of Object.keys(normalized)) {
        if (key.toLowerCase() === 'authorization')
            delete normalized[key];
    }
    normalized.Authorization = credential;
    return normalized;
}
/** @internal */
function prepareInitialRequest(input, init, callerHeaders, header, hasExplicitAcceptPayment, policy) {
    const shouldInjectAcceptPayment = Boolean(header) && !hasExplicitAcceptPayment && shouldInjectForPolicy(input, policy);
    if (!shouldInjectAcceptPayment)
        return { headers: callerHeaders, init, input };
    const headers = new Headers(input instanceof Request ? input.headers : undefined);
    callerHeaders.forEach((value, key) => {
        headers.set(key, value);
    });
    headers.set('Accept-Payment', header);
    if (init) {
        // Preserve init identity for callers like websocket upgrade helpers that
        // depend on the original RequestInit object reaching the underlying fetch.
        ;
        init.headers = headers;
        return {
            headers,
            init,
            input,
        };
    }
    return {
        headers,
        init: shouldInjectAcceptPayment ? { headers } : undefined,
        input,
    };
}
/** @internal */
function getCallerHeaders(input, headers) {
    if (headers)
        return new Headers(headers);
    return new Headers(input instanceof Request ? input.headers : undefined);
}
/** @internal */
function unwrapFetch(fetch) {
    let current = fetch;
    while (current[MPPX_FETCH_WRAPPER]) {
        current = current[MPPX_FETCH_WRAPPER];
    }
    return current;
}
/** @internal */
function isWrappedFetch(fetch) {
    return Boolean(fetch[MPPX_FETCH_WRAPPER]);
}
/** @internal */
function validateCredentialHeaderValue(credential) {
    if (!credential.trim())
        throw new Error('Credential header value must be non-empty');
    if (credential.includes('\r') || credential.includes('\n')) {
        throw new Error('Credential header value contains illegal newline characters');
    }
}
/** @internal */
async function resolveCredential(challenge, mi, context) {
    const parsedContext = mi.context && context !== undefined ? mi.context.parse(context) : undefined;
    return mi.createCredential(parsedContext !== undefined ? { challenge, context: parsedContext } : { challenge });
}
function resolvePaymentPreferences(headers, acceptPayment) {
    const header = headers.get('Accept-Payment');
    if (!header)
        return acceptPayment;
    try {
        return {
            ...acceptPayment,
            entries: AcceptPayment.parse(header),
            header,
        };
    }
    catch {
        // Fail open for explicit malformed headers: preserve the caller's header on
        // the wire, but continue automatic challenge selection with configured
        // defaults instead of throwing from the wrapper.
        return acceptPayment;
    }
}
/** @internal */
function shouldInjectForPolicy(input, policy) {
    if (policy === 'always')
        return true;
    if (policy === 'never')
        return false;
    const url = resolveRequestUrl(input);
    if (policy === 'same-origin') {
        if (!isBrowser())
            return true;
        return url.origin === globalThis.location.origin;
    }
    return policy.origins.some((origin) => matchesOrigin(url, origin));
}
/** @internal Matches an origin pattern, supporting `*.` prefix for subdomain wildcards. */
function matchesOrigin(url, pattern) {
    if (pattern.startsWith('*.')) {
        const suffix = pattern.slice(1); // e.g. ".example.com"
        return url.hostname.endsWith(suffix) || url.hostname === pattern.slice(2);
    }
    return url.origin === new URL(pattern).origin;
}
/** @internal */
function isBrowser() {
    return typeof globalThis.location !== 'undefined';
}
/** @internal */
function resolveRequestUrl(input) {
    if (input instanceof URL)
        return input;
    if (input instanceof Request)
        return new URL(input.url);
    return new URL(input, isBrowser() ? globalThis.location.href : undefined);
}
//# sourceMappingURL=Fetch.js.map