/**
 * route-cache.ts — 路由级响应缓存中间件工厂
 *
 * 职责：
 *   1. normalizeCacheOptions：统一 false/number/object → RouteCacheOptions | null
 *   2. defaultCacheKey：默认 key 生成（method:path?sortedQuery|varyHeaders）
 *   3. buildRouteCacheMiddleware：构建路由级响应缓存中间件
 *
 * 设计：
 *   - 缓存中间件插入在路由级中间件之后、validate 之前
 *   - HIT：直接发送 response-cache-kit 结果（跳过 validate + handler）
 *   - MISS：注册 res._onSend 钩子，handler 执行 res.json() 时捕获原始 data
 *   - 204 / 非 JSON 响应不缓存
 *   - 空 key 跳过缓存
 *
 * @module lib/middlewares/route-cache
 * @see 15-route-cache.md §4（内部架构）
 */
import { VEXT_CACHEABLE_STATUSES, createResponseCacheHeaders, createVextLegacyKey, normalizeResponseCacheRequest, } from "response-cache-kit";
// ── normalizeCacheOptions ──────────────────────────────────────
/**
 * 统一路由级 cache 配置
 *
 * @param cache RouteOptions.cache 原始值
 * @param globalDefaultTtl 全局默认 TTL（来自 config.cache.defaultTtl）
 * @returns 规范化的 RouteCacheOptions | null（null 表示不缓存）
 *
 * 规则：
 *   - `undefined` → null（未配置，不缓存）
 *   - `false`     → null（显式禁用）
 *   - `0` 或负值  → null（数字简写禁用）
 *   - `number > 0` → { ttl: number }（数字简写）
 *   - `{ ttl, ... }` → 原样返回（对象形式）
 */
export function normalizeCacheOptions(cache, globalDefaultTtl) {
    if (cache === undefined || cache === false) {
        return null;
    }
    if (typeof cache === "number") {
        if (cache <= 0)
            return null;
        return { ttl: cache };
    }
    // 对象形式
    if (!cache.ttl && globalDefaultTtl && globalDefaultTtl > 0) {
        return { ...cache, ttl: globalDefaultTtl };
    }
    if (cache.ttl <= 0) {
        return null;
    }
    return cache;
}
// ── defaultCacheKey ────────────────────────────────────────────
/**
 * 默认缓存 key 生成
 *
 * 格式: `${method}:${path}[?${sortedQuery}][|${varyValues}]`
 *
 * 设计原则:
 *   1. method + path 天然区分不同路由和动态参数
 *      (req.path = '/users/42' 而非 '/users/:id')
 *   2. query 参数排序确保 ?a=1&b=2 ≡ ?b=2&a=1
 *   3. vary headers 区分同路径不同语言/编码
 *   4. 不含 auth/cookie → 安全默认
 *
 * 示例:
 *   GET /products               → 'GET:/products'
 *   GET /products?limit=10&page=2 → 'GET:/products?limit=10&page=2'
 *   GET /products + zh-CN         → 'GET:/products|accept-language=zh-CN'
 */
export function defaultCacheKey(req, vary = []) {
    let key = `${req.method}:${req.path}`;
    const queryKeys = Object.keys(req.query);
    if (queryKeys.length > 0) {
        queryKeys.sort();
        key += "?" + queryKeys.map((k) => `${k}=${req.query[k]}`).join("&");
    }
    const varyHeaders = vary === "*" ? Object.keys(req.headers).sort() : vary;
    if (varyHeaders.length > 0) {
        for (const h of varyHeaders) {
            key += `|${h}=${req.headers[h.toLowerCase()] ?? ""}`;
        }
    }
    return key;
}
// ── buildRouteCacheMiddleware ──────────────────────────────────
/**
 * 构建路由级响应缓存中间件
 *
 * @param cacheOpts 规范化后的缓存配置（null 时返回 null，不构建中间件）
 * @param getResponseCache 延迟获取 ResponseCache 实例（避免在路由注册时实例尚未就绪）
 * @returns VextMiddleware | null
 */
export function buildRouteCacheMiddleware(cacheOpts, getResponseCache) {
    if (!cacheOpts)
        return null;
    const { ttl, key: keyFn, condition, vary = [], cacheControl = true, partitionKey, allowAuthorizationCache = false, } = cacheOpts;
    const cacheMiddleware = async (req, res, next) => {
        // ── condition 检查 ───────────────────────────────────
        if (condition && !condition(req)) {
            res.setHeader("X-Cache", "MISS");
            await next();
            return;
        }
        // ── 生成缓存 key ────────────────────────────────────
        const cacheKey = typeof keyFn === "function"
            ? keyFn(req)
            : typeof keyFn === "string"
                ? keyFn
                : defaultCacheKey(req, vary);
        // 空 key 跳过缓存
        if (!cacheKey) {
            await next();
            return;
        }
        const resolvedPartitionKey = resolvePartitionKey(req, partitionKey);
        const request = normalizeResponseCacheRequest({
            method: req.method,
            url: req.url || req.path,
            headers: req.headers,
            partitionKey: resolvedPartitionKey,
        });
        const requestAllowsCache = isCacheableRequest(req, resolvedPartitionKey, allowAuthorizationCache);
        const handleOptions = createHandleOptions(cacheKey, cacheOpts, keyFn !== undefined);
        const responseCache = getResponseCache();
        res.setHeader("X-Cache", "MISS");
        const origin = createOrigin(req, res, next, {
            requestAllowsCache,
            ttl,
            cacheControl,
        });
        const result = await responseCache.handle(request, origin, handleOptions);
        if (result.metadata.state === "hit" ||
            result.metadata.state === "deduped") {
            const headers = createVextHeaders(result, cacheControl);
            for (const [name, value] of Object.entries(headers)) {
                res.setHeader(name, value);
            }
            res.json(result.body, result.status);
        }
    };
    return cacheMiddleware;
}
function createHandleOptions(cacheKey, cacheOpts, hasCustomKey) {
    const options = {
        ttl: cacheOpts.ttl,
        vary: cacheOpts.vary ?? [],
        tags: cacheOpts.tags ?? [],
        cacheableStatuses: VEXT_CACHEABLE_STATUSES,
        allowAuthorizationCache: cacheOpts.allowAuthorizationCache === true,
        keyBuilder: hasCustomKey
            ? createVextCustomKeyBuilder(cacheKey)
            : createVextLegacyKey,
    };
    return options;
}
function createVextCustomKeyBuilder(baseKey) {
    return (request, context) => {
        const parts = [baseKey];
        if (request.partitionKey) {
            parts.push(`partition=${request.partitionKey}`);
        }
        const headers = normalizeRequestHeaders(request.headers);
        for (const rawName of context.vary) {
            const name = rawName.toLowerCase();
            const value = headers[name];
            if (value !== undefined) {
                parts.push(`${name}=${value}`);
            }
        }
        return parts.join("|");
    };
}
function createOrigin(req, res, next, options) {
    return () => new Promise((resolve, reject) => {
        let settled = false;
        const previousOnSend = res._onSend;
        res._onSend = (data, statusCode, headers = {}) => {
            previousOnSend?.(data, statusCode, headers);
            const responseHeaders = { ...headers };
            if (options.cacheControl &&
                options.requestAllowsCache &&
                shouldSetMissCacheControl(statusCode, responseHeaders)) {
                const value = `public, max-age=${Math.ceil(options.ttl / 1000)}`;
                res.setHeader("Cache-Control", value);
                responseHeaders["Cache-Control"] = value;
            }
            if (!settled) {
                settled = true;
                resolve({ body: data, status: statusCode, headers: responseHeaders });
            }
        };
        next()
            .then(() => {
            if (!settled) {
                settled = true;
                resolve({
                    body: undefined,
                    status: res.statusCode,
                    headers: { "Cache-Control": "no-store" },
                });
            }
        })
            .catch((error) => {
            if (!settled) {
                settled = true;
                reject(error);
                return;
            }
            reject(error);
        });
    });
}
function createVextHeaders(result, cacheControl) {
    const headers = {};
    for (const [name, value] of Object.entries(result.headers)) {
        const normalized = name.toLowerCase();
        if (normalized === "x-cache")
            continue;
        if (cacheControl && normalized === "cache-control")
            continue;
        headers[name] = value;
    }
    Object.assign(headers, createResponseCacheHeaders(result, {
        cacheControl,
        cacheHeaderName: false,
    }));
    headers["X-Cache"] =
        result.metadata.state === "hit" || result.metadata.state === "deduped"
            ? "HIT"
            : "MISS";
    const cacheControlValue = headers["Cache-Control"];
    if (cacheControlValue) {
        headers["Cache-Control"] = cacheControlValue.replace("public,max-age", "public, max-age");
    }
    return headers;
}
function resolvePartitionKey(req, partitionKey) {
    if (typeof partitionKey === "function") {
        const value = partitionKey(req);
        return value == null || value === "" ? undefined : String(value);
    }
    return partitionKey;
}
function isCacheableRequest(req, partitionKey, allowAuthorizationCache) {
    const method = req.method.toUpperCase();
    if (method !== "GET" && method !== "HEAD")
        return false;
    const cacheControl = req.headers["cache-control"] ?? "";
    if (hasCacheControlToken(cacheControl, "no-store"))
        return false;
    if (hasCacheControlToken(cacheControl, "no-cache"))
        return false;
    if (req.headers.authorization && !allowAuthorizationCache && !partitionKey) {
        return false;
    }
    return true;
}
function shouldSetMissCacheControl(statusCode, headers) {
    if (!VEXT_CACHEABLE_STATUSES.includes(statusCode))
        return false;
    if (headers["Cache-Control"] || headers["cache-control"])
        return false;
    if (headers["Set-Cookie"] || headers["set-cookie"])
        return false;
    return true;
}
function hasCacheControlToken(value, token) {
    return value
        .split(",")
        .map((part) => part.trim().toLowerCase())
        .includes(token);
}
function normalizeRequestHeaders(headers) {
    const normalized = {};
    if (!headers)
        return normalized;
    if (typeof headers.forEach === "function") {
        headers.forEach((value, key) => {
            normalized[key.toLowerCase()] = String(value);
        });
        return normalized;
    }
    if (Symbol.iterator in Object(headers)) {
        for (const [key, value] of headers) {
            if (value !== undefined && value !== null) {
                normalized[String(key).toLowerCase()] = String(value);
            }
        }
        return normalized;
    }
    for (const [key, value] of Object.entries(headers)) {
        if (value !== undefined && value !== null) {
            normalized[key.toLowerCase()] = Array.isArray(value)
                ? value.join(", ")
                : String(value);
        }
    }
    return normalized;
}
