import { Provider } from "../auth.js";
import { Scope } from "../scope.js";
import { isBinary } from "../serde.js";
import { memoize } from "../util/memoize.js";
import { withExponentialBackoff } from "../util/retry.js";
import { safeFetch } from "../util/safe-fetch.js";
import { CloudflareAuth } from "./auth.js";
import { getCloudflareAccountId, getUserEmailFromApiKey } from "./user.js";
/**
 * Creates a CloudflareApi instance with automatic account ID discovery if not provided
 *
 * @param options API options
 * @returns Promise resolving to a CloudflareApi instance
 */
export const createCloudflareApi = memoize(async (options = {}) => {
    const baseUrl = options.baseUrl ?? process.env.CLOUDFLARE_BASE_URL;
    const apiKey = options.apiKey?.unencrypted ?? process.env.CLOUDFLARE_API_KEY;
    const apiToken = options.apiToken?.unencrypted ?? process.env.CLOUDFLARE_API_TOKEN;
    const email = options.email ?? process.env.CLOUDFLARE_EMAIL;
    const accountId = options.accountId ??
        process.env.CLOUDFLARE_ACCOUNT_ID ??
        process.env.CF_ACCOUNT_ID;
    if (apiKey) {
        const credentials = {
            type: "api-key",
            apiKey,
            email: email ?? (await getUserEmailFromApiKey(apiKey)),
        };
        return new CloudflareApi({
            baseUrl: options.baseUrl,
            credentials,
            accountId: accountId ?? (await getCloudflareAccountId(credentials)),
        });
    }
    if (apiToken) {
        const credentials = {
            type: "api-token",
            apiToken,
        };
        return new CloudflareApi({
            baseUrl,
            credentials,
            accountId: accountId ?? (await getCloudflareAccountId(credentials)),
        });
    }
    try {
        const profile = options.profile ??
            Scope.getScope()?.profile ??
            process.env.CLOUDFLARE_PROFILE ??
            process.env.ALCHEMY_PROFILE ??
            "default";
        const { provider, credentials } = await Provider.getWithCredentials({
            provider: "cloudflare",
            profile,
        });
        return new CloudflareApi({
            baseUrl,
            profile,
            credentials,
            accountId: accountId ?? provider.metadata.id,
        });
    }
    catch (error) {
        throw new Error([
            "No credentials found. Please run `alchemy login`, or set either CLOUDFLARE_API_TOKEN or CLOUDFLARE_API_KEY in your environment.",
            "Learn more: https://alchemy.run/guides/cloudflare/",
        ].join("\n"), { cause: error });
    }
}, (options = {}) => [
    options.baseUrl,
    options.profile,
    options.apiKey?.unencrypted,
    options.apiToken?.unencrypted,
    options.accountId,
    options.email,
].join("|"));
/**
 * Cloudflare API client using raw fetch
 */
export class CloudflareApi {
    baseUrl;
    accountId;
    credentials;
    profile;
    /**
     * Create a new Cloudflare API client
     * Use createCloudflareApi factory function instead of direct constructor
     * for automatic account ID discovery.
     *
     * @param options API options
     */
    constructor(options) {
        this.baseUrl = options.baseUrl ?? "https://api.cloudflare.com/client/v4";
        this.accountId = options.accountId;
        this.credentials = options.credentials;
        this.profile = options.profile;
    }
    /**
     * Make a fetch request to the Cloudflare API
     *
     * @param path API path (without base URL)
     * @param init Fetch init options
     * @returns Raw Response object from fetch
     */
    async fetch(path, init = {}) {
        let headers = {
            "Content-Type": "application/json",
        };
        if (Array.isArray(init.headers)) {
            init.headers.forEach(([key, value]) => {
                headers[key] = value;
            });
        }
        else if (init.headers instanceof Headers) {
            init.headers.forEach((value, key) => {
                headers[key] = value;
            });
        }
        else if (init.headers) {
            headers = init.headers;
        }
        headers = {
            ...(await CloudflareAuth.formatHeadersWithRefresh({
                profile: this.profile,
                credentials: this.credentials,
            })),
            ...headers,
        };
        // TODO(sam): is this necessary?
        if (init.body instanceof FormData) {
            delete headers["Content-Type"];
        }
        let forbidden = false;
        // Use withExponentialBackoff for automatic retry on network errors
        return withExponentialBackoff(async () => {
            const response = await safeFetch(`${this.baseUrl}${path}`, {
                ...init,
                headers,
            });
            if (response.status.toString().startsWith("5")) {
                throw new InternalError(response.statusText);
            }
            if (response.status === 403 && !forbidden) {
                // we occasionally get 403s from Cloudflare tha tare actually transient
                // so, we will retry this at MOST once
                forbidden = true;
                throw new ForbiddenError();
            }
            if (response.status === 429) {
                const data = await response.json();
                throw new TooManyRequestsError(data.errors[0].message ?? response.statusText);
            }
            return response;
        }, 
        // transient errors should be retried aggressively
        (error) => error instanceof InternalError ||
            error instanceof TooManyRequestsError ||
            error instanceof ForbiddenError ||
            error.code === "ECONNRESET", 10, // Maximum 10 attempts (1 initial + 9 retries)
        1000);
    }
    /**
     * Helper for GET requests
     */
    async get(path, init = {}) {
        return this.fetch(path, { ...init, method: "GET" });
    }
    /**
     * Helper for HEAD requests
     */
    async head(path, init = {}) {
        return this.fetch(path, { ...init, method: "HEAD" });
    }
    /**
     * Helper for POST requests
     */
    async post(path, body, init = {}) {
        return this.fetch(path, {
            ...init,
            method: "POST",
            body: await this.toBody(body),
        });
    }
    /**
     * Helper for PUT requests
     */
    async put(path, body, init = {}) {
        return this.fetch(path, {
            ...init,
            method: "PUT",
            body: await this.toBody(body),
        });
    }
    async toBody(body) {
        return body instanceof FormData ||
            typeof body === "string" ||
            isBinary(body)
            ? body
            : JSON.stringify(body);
    }
    /**
     * Helper for PATCH requests
     */
    async patch(path, body, init = {}) {
        return this.fetch(path, {
            ...init,
            method: "PATCH",
            body: await this.toBody(body),
        });
    }
    /**
     * Helper for DELETE requests
     */
    async delete(path, init = {}) {
        return this.fetch(path, { ...init, method: "DELETE" });
    }
}
class InternalError extends Error {
}
class TooManyRequestsError extends Error {
    constructor(message) {
        super(`Cloudflare Rate Limit Exceeded at ${new Date().toISOString()}: ${message}`);
    }
}
class ForbiddenError extends Error {
}
//# sourceMappingURL=api.js.map