"""request_id 关联中间件（ARCH §9.3 / CLAUDE.md）。

每请求生成/透传 request_id，绑入 structlog contextvars，并回写响应头，
错误信封也带上同一 id，便于端到端 grep。
"""

from __future__ import annotations

import re
import uuid
from collections.abc import Awaitable, Callable

import structlog
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from ww_shared import ErrorBody, ErrorCode, ErrorEnvelope

REQUEST_ID_HEADER = "x-request-id"

# 请求体大小上限（2 MiB）：超过即 413，防超大 body 撑爆内存/成本（CR-H9）。
MAX_BODY_BYTES = 2 * 1024 * 1024

# 放行安全字符集（字母数字 . _ -，1–128 位）：覆盖 uuid4().hex、常见 trace id 与客户端
# 自定义短 id；含空白/控制字符/换行/注入序列/超长的一律丢弃改生成新 uuid——防止把未经
# 校验的客户端值原样写进日志/响应头（日志注入防护）。
_REQUEST_ID_PATTERN = re.compile(r"^[A-Za-z0-9._-]{1,128}$")


def _sanitize_request_id(raw: str | None) -> str:
    if raw is not None and _REQUEST_ID_PATTERN.match(raw):
        return raw
    return uuid.uuid4().hex


async def request_id_middleware(
    request: Request, call_next: Callable[[Request], Awaitable[Response]]
) -> Response:
    request_id = _sanitize_request_id(request.headers.get(REQUEST_ID_HEADER))
    structlog.contextvars.clear_contextvars()
    structlog.contextvars.bind_contextvars(request_id=request_id)
    request.state.request_id = request_id
    response = await call_next(request)
    response.headers[REQUEST_ID_HEADER] = request_id
    return response


async def body_size_limit_middleware(
    request: Request, call_next: Callable[[Request], Awaitable[Response]]
) -> Response:
    """按 `Content-Length` 拦截超大请求体，超限直接 413（CR-H9）。

    用户中间件跑在 FastAPI 的 AppError 处理器**之外**，故必须自建错误信封响应（不能靠抛
    AppError）。分块编码（无 `Content-Length`）无法在此拦截——单用户原型可接受的限制。
    """
    content_length = request.headers.get("content-length")
    if content_length is not None:
        try:
            size = int(content_length)
        except ValueError:
            size = 0  # 非法头防御性视作 0，交由下游正常解析/报错。
        if size > MAX_BODY_BYTES:
            envelope = ErrorEnvelope(
                error=ErrorBody(
                    code=ErrorCode.PAYLOAD_TOO_LARGE,
                    message="请求体过大",
                    request_id=_sanitize_request_id(request.headers.get(REQUEST_ID_HEADER)),
                )
            )
            return JSONResponse(status_code=413, content=envelope.model_dump())
    return await call_next(request)
