import { Readable } from "node:stream";
import { requestContext } from "./request-context.js";
// ── 幂等方法集合（用于判断是否可重试）─────────────────────────
const IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "OPTIONS", "PUT", "DELETE"]);
const AUTHORIZATION_HEADER = "authorization";
const HOP_BY_HOP_HEADERS = new Set([
    "connection",
    "keep-alive",
    "proxy-authenticate",
    "proxy-authorization",
    "te",
    "trailer",
    "transfer-encoding",
    "upgrade",
]);
const BODYLESS_STATUS = new Set([204, 304]);
// ── 核心实现 ────────────────────────────────────────────────
/**
 * createVextFetch — 创建 app.fetch 内置 HTTP 客户端
 *
 * 在 bootstrap 阶段调用，将返回的 VextFetch 挂载到 app.fetch。
 *
 * @param logger          app.logger 实例（用于结构化日志）
 * @param fetchConfig     config.fetch 配置
 * @param requestIdHeader requestId 传播使用的头名称（默认 'x-request-id'）
 * @returns VextFetch 实例
 */
export function createVextFetch(logger, fetchConfig = {}, requestIdHeader = "x-request-id") {
    const globalTimeout = fetchConfig.timeout ?? 10_000;
    const globalRetry = fetchConfig.retry ?? 0;
    const globalRetryDelay = fetchConfig.retryDelay ?? 1000;
    const globalPropagateHeaders = fetchConfig.propagateHeaders ?? [];
    const proxyTargets = fetchConfig.proxy ?? [];
    /**
     * 核心 fetch 函数
     */
    async function vextFetch(input, init) {
        const url = typeof input === "string"
            ? input
            : input instanceof URL
                ? input.href
                : input.url;
        const method = (init?.method ?? "GET").toUpperCase();
        const timeout = init?.timeout ?? globalTimeout;
        const propagate = init?.propagateRequestId !== false;
        // ── 1. 构建请求头（注入追踪头）────────────────────────
        const headers = new Headers(init?.headers);
        const store = requestContext.getStore();
        // ── 1a. 注入 requestId（受 propagateRequestId 控制）──
        if (propagate && store?.requestId && !headers.has(requestIdHeader)) {
            headers.set(requestIdHeader, store.requestId);
        }
        // ── 1b. 透传 propagatedHeaders（始终生效，不受 propagateRequestId 控制）──
        // store.propagatedHeaders 由 request-id 中间件在入站请求阶段
        // 从原始请求头中捕获并写入（根据 config.fetch.propagateHeaders 列表）。
        // 此处从 store 中读取并注入到出站请求头，实现"入站头 → 出站头"完整透传链路。
        //
        // 优先级：init.headers 手动设置 > store.propagatedHeaders（不覆盖用户显式设置的头）
        //
        // 单次请求可通过 init.propagateHeaders 指定额外透传头（已在入站阶段写入 store，
        // 但仅当 request-id 中间件的 propagateHeaderNames 包含该头时才有值）。
        // 如需透传未在全局配置中声明的头，直接在 init.headers 中手动设置即可。
        if (store?.propagatedHeaders) {
            for (const [key, value] of Object.entries(store.propagatedHeaders)) {
                if (!headers.has(key)) {
                    headers.set(key, value);
                }
            }
        }
        // ── 2. 确定重试配置 ──────────────────────────────────
        const maxRetries = IDEMPOTENT_METHODS.has(method)
            ? (init?.retry ?? globalRetry)
            : 0;
        const retryDelay = init?.retryDelay ?? globalRetryDelay;
        // ── 3. 执行请求（含重试循环）────────────────────────
        let lastError = null;
        for (let attempt = 0; attempt <= maxRetries; attempt++) {
            // 重试等待（首次请求不等待）
            if (attempt > 0) {
                const delay = typeof retryDelay === "function" ? retryDelay(attempt) : retryDelay;
                await sleep(delay);
                logger.debug({
                    type: "outbound",
                    method,
                    url,
                    attempt,
                    maxRetries,
                }, `→ ${method} ${url} RETRY attempt ${attempt}/${maxRetries}`);
            }
            // ── 超时控制 ──────────────────────────────────────
            const controller = new AbortController();
            const timer = setTimeout(() => controller.abort(), timeout);
            // 合并用户 signal 和超时 signal
            const signal = mergeSignals(init?.signal ?? null, controller.signal);
            const startTime = performance.now();
            try {
                const response = await fetch(input, {
                    ...init,
                    method,
                    headers,
                    signal,
                });
                clearTimeout(timer);
                const duration = Math.round(performance.now() - startTime);
                // ── 日志记录 ────────────────────────────────────
                const level = response.ok
                    ? "debug"
                    : response.status >= 500
                        ? "error"
                        : "warn";
                logger[level]({
                    type: "outbound",
                    method,
                    url,
                    status: response.status,
                    duration,
                    requestId: requestContext.getStore()?.requestId,
                }, `→ ${method} ${url} ${response.status} ${duration}ms`);
                // 非幂等方法 或 非服务端错误 → 不重试，直接返回
                if (!IDEMPOTENT_METHODS.has(method) || response.status < 500) {
                    return response;
                }
                // 幂等方法 + 5xx → 如果还有重试机会则继续
                if (attempt < maxRetries) {
                    lastError = new Error(`[app.fetch] ${method} ${url} returned ${response.status}`);
                    continue;
                }
                // 最后一次重试也失败了，返回响应（让调用方处理）
                return response;
            }
            catch (err) {
                clearTimeout(timer);
                const duration = Math.round(performance.now() - startTime);
                const error = err instanceof Error ? err : new Error(String(err));
                if (error.name === "AbortError") {
                    logger.error({
                        type: "outbound",
                        method,
                        url,
                        error: "timeout",
                        duration,
                        timeout,
                        requestId: requestContext.getStore()?.requestId,
                    }, `→ ${method} ${url} TIMEOUT ${duration}ms (limit: ${timeout}ms)`);
                    // 超时不重试
                    throw new Error(`[app.fetch] ${method} ${url} timed out after ${timeout}ms`);
                }
                logger.error({
                    type: "outbound",
                    method,
                    url,
                    error: error.message,
                    duration,
                    requestId: requestContext.getStore()?.requestId,
                }, `→ ${method} ${url} ERROR ${duration}ms: ${error.message}`);
                lastError = error;
                // 还有重试机会且是幂等方法 → 继续
                if (attempt < maxRetries && IDEMPOTENT_METHODS.has(method)) {
                    continue;
                }
                throw error;
            }
        }
        // 理论上不应到达此处，但作为防御性编码
        throw lastError ?? new Error(`[app.fetch] ${method} ${url} failed`);
    }
    // ── 快捷方法 ──────────────────────────────────────────────
    vextFetch.get = (url, init) => vextFetch(url, { ...init, method: "GET" });
    vextFetch.post = (url, body, init) => vextFetch(url, {
        ...init,
        method: "POST",
        body: body != null ? JSON.stringify(body) : undefined,
        headers: {
            ...(body != null ? { "content-type": "application/json" } : {}),
            ...init?.headers,
        },
    });
    vextFetch.put = (url, body, init) => vextFetch(url, {
        ...init,
        method: "PUT",
        body: body != null ? JSON.stringify(body) : undefined,
        headers: {
            ...(body != null ? { "content-type": "application/json" } : {}),
            ...init?.headers,
        },
    });
    vextFetch.patch = (url, body, init) => vextFetch(url, {
        ...init,
        method: "PATCH",
        body: body != null ? JSON.stringify(body) : undefined,
        headers: {
            ...(body != null ? { "content-type": "application/json" } : {}),
            ...init?.headers,
        },
    });
    vextFetch.delete = (url, init) => vextFetch(url, { ...init, method: "DELETE" });
    // ── create() 工厂 ────────────────────────────────────────
    vextFetch.create = (options) => {
        const childFetchConfig = {
            timeout: options.timeout ?? globalTimeout,
            retry: options.retry ?? globalRetry,
            retryDelay: options.retryDelay ?? globalRetryDelay,
            propagateHeaders: globalPropagateHeaders,
        };
        // 创建子 VextFetch（递归使用 createVextFetch）
        const child = createVextFetch(logger, childFetchConfig, requestIdHeader);
        // 包装：自动拼接 baseURL + 合并默认 headers
        const baseURL = options.baseURL.replace(/\/+$/, "");
        const defaultHeaders = options.headers ?? {};
        const wrappedFetch = ((input, init) => {
            const resolvedInput = typeof input === "string"
                ? `${baseURL}${input.startsWith("/") ? "" : "/"}${input}`
                : input;
            return child(resolvedInput, {
                ...init,
                headers: {
                    ...defaultHeaders,
                    ...init?.headers,
                },
            });
        });
        // 快捷方法也拼接 baseURL
        wrappedFetch.get = (url, init) => wrappedFetch(url, { ...init, method: "GET" });
        wrappedFetch.post = (url, body, init) => wrappedFetch(url, {
            ...init,
            method: "POST",
            body: body != null ? JSON.stringify(body) : undefined,
            headers: {
                ...(body != null ? { "content-type": "application/json" } : {}),
                ...init?.headers,
            },
        });
        wrappedFetch.put = (url, body, init) => wrappedFetch(url, {
            ...init,
            method: "PUT",
            body: body != null ? JSON.stringify(body) : undefined,
            headers: {
                ...(body != null ? { "content-type": "application/json" } : {}),
                ...init?.headers,
            },
        });
        wrappedFetch.patch = (url, body, init) => wrappedFetch(url, {
            ...init,
            method: "PATCH",
            body: body != null ? JSON.stringify(body) : undefined,
            headers: {
                ...(body != null ? { "content-type": "application/json" } : {}),
                ...init?.headers,
            },
        });
        wrappedFetch.delete = (url, init) => wrappedFetch(url, { ...init, method: "DELETE" });
        // create() 也可以在子实例上再调用
        wrappedFetch.create = vextFetch.create;
        return wrappedFetch;
    };
    vextFetch.proxy = createFetchProxy({
        logger,
        targets: proxyTargets,
        timeout: globalTimeout,
        retry: globalRetry,
        retryDelay: globalRetryDelay,
    });
    return vextFetch;
}
class ProxyLocalError extends Error {
    status;
    code;
    constructor(status, code, message) {
        super(message);
        this.status = status;
        this.code = code;
        this.name = "ProxyLocalError";
    }
}
class ProxyTimeoutError extends Error {
    timeout;
    constructor(message, timeout) {
        super(message);
        this.timeout = timeout;
        this.name = "ProxyTimeoutError";
    }
}
class ProxyClientAbortError extends Error {
    constructor() {
        super("Client aborted proxy request");
        this.name = "ProxyClientAbortError";
    }
}
function createFetchProxy(runtime) {
    const targetMap = new Map();
    for (const target of runtime.targets) {
        targetMap.set(target.name, target);
    }
    const directProxy = (async (req, res, options) => {
        await handleProxyRequest(runtime, req, res, undefined, options);
    });
    return new Proxy(directProxy, {
        get(target, prop, receiver) {
            if (typeof prop !== "string") {
                return Reflect.get(target, prop, receiver);
            }
            if (prop === "then") {
                return undefined;
            }
            const configuredTarget = targetMap.get(prop);
            if (configuredTarget) {
                return async (req, res, options) => {
                    await handleProxyRequest(runtime, req, res, configuredTarget, options);
                };
            }
            if (Reflect.has(target, prop)) {
                return Reflect.get(target, prop, receiver);
            }
            return async (req, res) => {
                writeProxyLocalError(req, res, 500, "FETCH_PROXY_TARGET_NOT_FOUND", `[app.fetch.proxy] target "${prop}" is not configured.`);
            };
        },
    });
}
async function handleProxyRequest(runtime, req, res, target, options) {
    try {
        const resolved = await resolveProxyRequest(runtime, req, target, options);
        const response = await fetchProxyWithRetry(runtime, req, resolved);
        await writeProxyResponse(response, res);
    }
    catch (err) {
        if (err instanceof ProxyClientAbortError) {
            runtime.logger.debug({
                type: "proxy",
                requestId: req.requestId,
                event: "client_abort",
            }, "[app.fetch.proxy] client aborted request");
            return;
        }
        if (err instanceof ProxyLocalError) {
            writeProxyLocalError(req, res, err.status, err.code, err.message);
            return;
        }
        if (err instanceof ProxyTimeoutError) {
            writeProxyLocalError(req, res, 504, "FETCH_PROXY_TIMEOUT", err.message);
            return;
        }
        const error = err instanceof Error ? err : new Error(String(err));
        runtime.logger.error({
            type: "proxy",
            requestId: req.requestId,
            error: error.message,
        }, `[app.fetch.proxy] upstream request failed: ${error.message}`);
        writeProxyLocalError(req, res, 502, "FETCH_PROXY_UPSTREAM_ERROR", "Upstream request failed.");
    }
}
async function resolveProxyRequest(runtime, req, target, options) {
    const proxyOptions = options ?? {};
    const method = (proxyOptions.method ?? req.method ?? "GET").toUpperCase();
    const url = resolveProxyUrl(target, proxyOptions);
    applyProxyQuery(url, req.query, proxyOptions.query);
    const headers = await resolveProxyHeaders(req, target, proxyOptions);
    const body = await resolveProxyBody(req, method, proxyOptions);
    const retry = normalizeProxyRetry(proxyOptions.retry ?? target?.retry ?? runtime.retry, "retry");
    const replayableBody = isReplayableProxyBody(body);
    return {
        target,
        targetName: target?.name ?? "direct",
        url: url.href,
        method,
        headers,
        body,
        timeout: normalizeProxyTimeout(proxyOptions.timeout ?? target?.timeout ?? runtime.timeout, "timeout"),
        retry,
        retryDelay: normalizeProxyRetryDelay(proxyOptions.retryDelay ?? target?.retryDelay ?? runtime.retryDelay, "retryDelay"),
        replayableBody,
    };
}
function resolveProxyUrl(target, options) {
    if (!target) {
        if (!options.url) {
            throw new ProxyLocalError(400, "FETCH_PROXY_URL_REQUIRED", "[app.fetch.proxy] options.url is required for direct proxy calls.");
        }
        try {
            return new URL(options.url);
        }
        catch {
            throw new ProxyLocalError(400, "FETCH_PROXY_INVALID_URL", "[app.fetch.proxy] options.url must be a valid absolute URL.");
        }
    }
    if (!options.path) {
        throw new ProxyLocalError(400, "FETCH_PROXY_PATH_REQUIRED", `[app.fetch.proxy.${target.name}] options.path is required.`);
    }
    const baseURL = target.baseURL.replace(/\/+$/, "");
    const path = options.path.replace(/^\/+/, "");
    return new URL(`${baseURL}/${path}`);
}
function applyProxyQuery(url, reqQuery, optionQuery) {
    for (const [key, value] of Object.entries(reqQuery)) {
        if (value !== undefined && value !== null) {
            url.searchParams.set(key, String(value));
        }
    }
    if (!optionQuery)
        return;
    for (const [key, value] of Object.entries(optionQuery)) {
        if (value === undefined || value === null) {
            url.searchParams.delete(key);
            continue;
        }
        url.searchParams.set(key, String(value));
    }
}
async function resolveProxyHeaders(req, target, options) {
    const headers = new Headers();
    const headerContext = { req, target, options };
    applyStaticHeaders(headers, target?.headers);
    const forwardHeaders = mergeHeaderNames(target?.forwardHeaders, options.forwardHeaders);
    const allowAuthorization = target?.allowAuthorizationForward === true ||
        options.allowAuthorizationForward === true;
    if (forwardHeaders.includes(AUTHORIZATION_HEADER) && !allowAuthorization) {
        throw new ProxyLocalError(400, "FETCH_PROXY_AUTHORIZATION_FORWARD_FORBIDDEN", "[app.fetch.proxy] forwarding authorization requires allowAuthorizationForward: true.");
    }
    for (const headerName of forwardHeaders) {
        const value = req.headers[headerName];
        if (value !== undefined) {
            headers.set(headerName, value);
        }
    }
    await applyProxyHeaders(headers, target?.defaultInjectHeaders, headerContext);
    applyStaticHeaders(headers, options.headers);
    await applyProxyHeaders(headers, options.injectHeaders, headerContext);
    return headers;
}
function mergeHeaderNames(...groups) {
    const seen = new Set();
    const result = [];
    for (const group of groups) {
        for (const name of group ?? []) {
            const normalized = name.trim().toLowerCase();
            if (!normalized || seen.has(normalized))
                continue;
            seen.add(normalized);
            result.push(normalized);
        }
    }
    return result;
}
function applyStaticHeaders(headers, source) {
    if (!source)
        return;
    for (const [key, value] of Object.entries(source)) {
        if (value !== undefined && value !== null) {
            headers.set(key, String(value));
        }
    }
}
async function applyProxyHeaders(headers, source, ctx) {
    if (!source)
        return;
    const resolved = typeof source === "function" ? await source(ctx) : source;
    applyStaticHeaders(headers, resolved);
}
async function resolveProxyBody(req, method, options) {
    if (method === "GET" || method === "HEAD") {
        return undefined;
    }
    if (options.body !== undefined) {
        return options.body;
    }
    return req._getRawBodyBuffer(options.maxBodySize);
}
function normalizeProxyTimeout(value, name) {
    if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
        throw new ProxyLocalError(400, "FETCH_PROXY_INVALID_TIMEOUT", `[app.fetch.proxy] ${name} must be a positive number.`);
    }
    return value;
}
function normalizeProxyRetry(value, name) {
    if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
        throw new ProxyLocalError(400, "FETCH_PROXY_INVALID_RETRY", `[app.fetch.proxy] ${name} must be a non-negative integer.`);
    }
    return value;
}
function normalizeProxyRetryDelay(value, name) {
    if (typeof value === "function") {
        return value;
    }
    if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
        throw new ProxyLocalError(400, "FETCH_PROXY_INVALID_RETRY_DELAY", `[app.fetch.proxy] ${name} must be a non-negative number or function.`);
    }
    return value;
}
function isReplayableProxyBody(body) {
    if (body === undefined || body === null)
        return true;
    if (typeof body === "string")
        return true;
    if (body instanceof Uint8Array)
        return true;
    if (body instanceof URLSearchParams)
        return true;
    if (typeof FormData !== "undefined" && body instanceof FormData) {
        return true;
    }
    if (typeof Blob !== "undefined" && body instanceof Blob) {
        return true;
    }
    return false;
}
async function fetchProxyWithRetry(runtime, req, resolved) {
    const retryableMethod = IDEMPOTENT_METHODS.has(resolved.method);
    const maxRetries = retryableMethod && resolved.replayableBody ? resolved.retry : 0;
    let clientAborted = false;
    let currentController = null;
    req.onClose(() => {
        clientAborted = true;
        currentController?.abort();
    });
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
        if (attempt > 0) {
            if (clientAborted) {
                throw new ProxyClientAbortError();
            }
            const delay = typeof resolved.retryDelay === "function"
                ? resolved.retryDelay(attempt)
                : resolved.retryDelay;
            await sleep(delay);
            if (clientAborted) {
                throw new ProxyClientAbortError();
            }
            runtime.logger.debug({
                type: "proxy",
                target: resolved.targetName,
                method: resolved.method,
                url: resolved.url,
                attempt,
                maxRetries,
                requestId: req.requestId,
            }, `[app.fetch.proxy] retry ${attempt}/${maxRetries}: ${resolved.method} ${resolved.url}`);
        }
        if (clientAborted) {
            throw new ProxyClientAbortError();
        }
        let timedOut = false;
        const controller = new AbortController();
        currentController = controller;
        const timer = setTimeout(() => {
            timedOut = true;
            controller.abort();
        }, resolved.timeout);
        const startTime = performance.now();
        try {
            const response = await fetch(resolved.url, {
                method: resolved.method,
                headers: resolved.headers,
                body: resolved.body,
                signal: controller.signal,
            });
            clearTimeout(timer);
            const duration = Math.round(performance.now() - startTime);
            const level = response.ok
                ? "debug"
                : response.status >= 500
                    ? "error"
                    : "warn";
            runtime.logger[level]({
                type: "proxy",
                target: resolved.targetName,
                method: resolved.method,
                url: resolved.url,
                status: response.status,
                duration,
                requestId: req.requestId,
            }, `[app.fetch.proxy] ${resolved.method} ${resolved.url} ${response.status} ${duration}ms`);
            if (response.status >= 500 &&
                retryableMethod &&
                resolved.replayableBody &&
                attempt < maxRetries) {
                try {
                    await response.body?.cancel();
                }
                catch {
                    // best-effort cleanup only
                }
                if (currentController === controller) {
                    currentController = null;
                }
                continue;
            }
            // Keep the controller attached after headers arrive so req.onClose()
            // can still abort an in-flight streamed response body.
            return response;
        }
        catch (err) {
            clearTimeout(timer);
            if (currentController === controller) {
                currentController = null;
            }
            const error = err instanceof Error ? err : new Error(String(err));
            if (isAbortError(error)) {
                if (clientAborted) {
                    throw new ProxyClientAbortError();
                }
                if (timedOut) {
                    throw new ProxyTimeoutError(`[app.fetch.proxy] ${resolved.method} ${resolved.url} timed out after ${resolved.timeout}ms.`, resolved.timeout);
                }
            }
            runtime.logger.error({
                type: "proxy",
                target: resolved.targetName,
                method: resolved.method,
                url: resolved.url,
                error: error.message,
                requestId: req.requestId,
            }, `[app.fetch.proxy] ${resolved.method} ${resolved.url} ERROR: ${error.message}`);
            if (retryableMethod && resolved.replayableBody && attempt < maxRetries) {
                continue;
            }
            throw error;
        }
    }
    throw new Error(`[app.fetch.proxy] ${resolved.method} ${resolved.url} failed.`);
}
async function writeProxyResponse(response, res) {
    const contentType = response.headers.get("content-type") ?? undefined;
    const isJson = contentType?.includes("application/json") === true ||
        contentType?.includes("+json") === true;
    const isText = contentType?.startsWith("text/") === true;
    res.status(response.status);
    if (BODYLESS_STATUS.has(response.status)) {
        copyProxyResponseHeaders(response, res, false);
        res.text("", response.status);
        return;
    }
    if (isJson || isText || !response.body) {
        const body = await response.text();
        copyProxyResponseHeaders(response, res, false);
        res.text(body, response.status);
        return;
    }
    copyProxyResponseHeaders(response, res, true);
    const stream = Readable.fromWeb(response.body);
    res.stream(stream, contentType ?? "application/octet-stream");
}
function copyProxyResponseHeaders(response, res, keepContentEncoding) {
    response.headers.forEach((value, key) => {
        const lower = key.toLowerCase();
        if (HOP_BY_HOP_HEADERS.has(lower))
            return;
        if (lower === "content-length")
            return;
        if (!keepContentEncoding && lower === "content-encoding")
            return;
        res.setHeader(key, value);
    });
}
function writeProxyLocalError(req, res, status, code, message) {
    res.rawJson({
        code,
        message,
        requestId: req.requestId,
    }, status);
}
function isAbortError(error) {
    return error.name === "AbortError";
}
// ── 辅助函数 ────────────────────────────────────────────────
/**
 * 合并两个 AbortSignal
 *
 * 任意一个 signal 触发 abort 时，返回的 signal 也触发 abort。
 * 如果 userSignal 为 null，直接返回 timeoutSignal。
 *
 * Node.js 20+ 支持 AbortSignal.any()，但为兼容 Node 18 手动实现。
 *
 * @param userSignal    用户传入的 signal（可为 null）
 * @param timeoutSignal 超时控制的 signal
 * @returns 合并后的 AbortSignal
 */
function mergeSignals(userSignal, timeoutSignal) {
    if (!userSignal)
        return timeoutSignal;
    // 尝试使用原生 AbortSignal.any()（Node.js 20+）
    if (typeof AbortSignal.any === "function") {
        return AbortSignal.any([userSignal, timeoutSignal]);
    }
    // Node.js 18 手动实现
    const controller = new AbortController();
    const onAbort = () => {
        controller.abort();
    };
    if (userSignal.aborted || timeoutSignal.aborted) {
        controller.abort();
        return controller.signal;
    }
    userSignal.addEventListener("abort", onAbort, { once: true });
    timeoutSignal.addEventListener("abort", onAbort, { once: true });
    return controller.signal;
}
/**
 * sleep — 延迟指定毫秒
 *
 * @param ms 毫秒数
 */
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
