from __future__ import annotations

import hashlib
from collections.abc import Awaitable, Callable

from fastapi import Request
from starlette.responses import Response

JSON_CACHE_CONTROL = "private, no-cache"
JSON_ETAG_VARY_HEADERS = ("Authorization", "Cookie", "X-API-Key", "Accept-Language")


async def conditional_json_get_middleware(
    request: Request,
    call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
    """Attach ETags to JSON GET responses and return 304 when unchanged.

    The middleware deliberately works after route handling. That keeps the
    contract platform-wide without requiring every module router to learn about
    conditional requests, while still limiting buffering to successful JSON GET
    responses.
    """

    response = await call_next(request)
    if not _eligible_for_conditional_json_get(request, response):
        return response

    body = b"".join([chunk async for chunk in response.body_iterator])
    etag = response.headers.get("etag") or json_response_etag(body)
    headers = dict(response.headers)
    headers["etag"] = etag
    headers["cache-control"] = _conditional_cache_control(headers.get("cache-control"))
    headers["vary"] = _merge_vary(headers.get("vary"), JSON_ETAG_VARY_HEADERS)
    headers.pop("content-length", None)

    if if_none_match_matches(request.headers.get("if-none-match"), etag):
        return Response(status_code=304, headers=_not_modified_headers(headers), background=response.background)

    return Response(content=body, status_code=response.status_code, headers=headers, background=response.background)


def json_response_etag(body: bytes) -> str:
    digest = hashlib.sha256(body).hexdigest()
    return f'W/"sha256-{digest}"'


def if_none_match_matches(header_value: str | None, etag: str) -> bool:
    if not header_value:
        return False
    normalized_etag = _normalize_etag_for_weak_compare(etag)
    for raw_candidate in header_value.split(","):
        candidate = raw_candidate.strip()
        if candidate == "*":
            return True
        if _normalize_etag_for_weak_compare(candidate) == normalized_etag:
            return True
    return False


def _eligible_for_conditional_json_get(request: Request, response: Response) -> bool:
    if request.method.upper() != "GET":
        return False
    if response.status_code != 200:
        return False
    if "set-cookie" in response.headers:
        return False
    if "content-encoding" in response.headers:
        return False
    if "content-disposition" in response.headers:
        return False
    if "no-store" in request.headers.get("cache-control", "").lower():
        return False
    if "no-store" in response.headers.get("cache-control", "").lower():
        return False
    return _is_json_content_type(response.headers.get("content-type"))


def _is_json_content_type(value: str | None) -> bool:
    media_type = (value or "").split(";", 1)[0].strip().lower()
    return media_type == "application/json" or media_type.endswith("+json")


def _conditional_cache_control(current: str | None) -> str:
    if not current:
        return JSON_CACHE_CONTROL
    directives = [item.strip() for item in current.split(",") if item.strip()]
    normalized = {item.split("=", 1)[0].strip().lower() for item in directives}
    if "private" not in normalized and "public" not in normalized:
        directives.append("private")
    if "no-cache" not in normalized:
        directives.append("no-cache")
    return ", ".join(directives)


def _merge_vary(current: str | None, additions: tuple[str, ...]) -> str:
    tokens: list[str] = []
    seen: set[str] = set()
    for value in [current or "", *additions]:
        for token in value.split(","):
            stripped = token.strip()
            if not stripped:
                continue
            normalized = stripped.lower()
            if normalized in seen:
                continue
            seen.add(normalized)
            tokens.append(stripped)
    return ", ".join(tokens)


def _not_modified_headers(headers: dict[str, str]) -> dict[str, str]:
    allowed = {"cache-control", "content-location", "date", "etag", "expires", "vary", "x-correlation-id"}
    return {key: value for key, value in headers.items() if key.lower() in allowed}


def _normalize_etag_for_weak_compare(value: str) -> str:
    normalized = value.strip()
    if normalized.startswith("W/"):
        normalized = normalized[2:].strip()
    if len(normalized) >= 2 and normalized[0] == '"' and normalized[-1] == '"':
        normalized = normalized[1:-1]
    return normalized
