"""Correlation ID propagation across HTTP boundaries (gap #1).

Contextvars propagate correlation IDs across `await` boundaries in the same
task, but they do NOT cross thread pools — so an in-process `correlation_scope()`
is invisible to a downstream service we call via HTTP. This middleware closes
that gap using the standard `X-Correlation-Id` header:

    Request in     → read X-Correlation-Id (or generate one)
                     set contextvar for the duration of the request
    Response out   → echo the same ID back in X-Correlation-Id

Every Decision, ActivityEvent, and log entry produced inside the request
handler inherits the ID automatically because they all read from the
contextvar. When the agent later calls out to another service or logs an
event, the ID is visible to the other side.

The middleware is safe to run ahead of the contract/activity middleware —
it only touches the contextvar and the response headers, never the body.
"""

from __future__ import annotations

import logging

from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware

from agntspace.activity.decisions import (
    _restore_authorized_actions,
    new_correlation_id,
    reset_authorized_actions,
    reset_correlation_id,
    set_correlation_id,
)

log = logging.getLogger(__name__)

HEADER_NAME = "X-Correlation-Id"
MAX_HEADER_LEN = 128  # reject absurdly long values


class CorrelationMiddleware(BaseHTTPMiddleware):
    """Read / generate a correlation ID for every HTTP request."""

    async def dispatch(self, request: Request, call_next) -> Response:  # noqa: ANN001
        # Prefer the header from the client; fall back to generating one
        incoming = request.headers.get(HEADER_NAME, "").strip()
        if incoming and len(incoming) > MAX_HEADER_LEN:
            incoming = incoming[:MAX_HEADER_LEN]
        if not incoming:
            incoming = new_correlation_id("http")

        # Start a fresh scope for this request. Reset the authorized
        # contract-actions set so it can't leak from whatever async
        # context spawned us — every request starts guarded from zero.
        cid_token = set_correlation_id(incoming)
        auth_token = reset_authorized_actions()
        try:
            response = await call_next(request)
        finally:
            _restore_authorized_actions(auth_token)
            reset_correlation_id(cid_token)

        # Echo the ID back so the client can correlate logs on their side
        response.headers[HEADER_NAME] = incoming
        return response
