import uuid
from datetime import datetime

from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from injectq import InjectQ
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware.cors import CORSMiddleware
from starlette.requests import Request
from starlette.types import ASGIApp, Receive, Scope, Send

from agentflow_cli.src.app.core.middleware.rate_limit import RateLimitMiddleware, build_backend
from agentflow_cli.src.app.core.middleware.request_limits import RequestSizeLimitMiddleware
from agentflow_cli.src.app.core.middleware.security_headers import SecurityHeadersMiddleware

from .graph_config import GraphConfig
from .sentry_config import init_sentry
from .settings import get_settings, logger


# Paths that should be excluded from GZip compression (streaming endpoints)
GZIP_EXCLUDED_PATHS = frozenset({"/v1/graph/stream"})


class SelectiveGZipMiddleware:
    """
    GZip middleware that excludes certain paths from compression.

    This is necessary because streaming endpoints need to send data
    immediately without buffering, but GZipMiddleware buffers the
    entire response before compressing.
    """

    def __init__(self, app: ASGIApp, minimum_size: int = 1000):
        self.app = app
        self.gzip_app = GZipMiddleware(app, minimum_size=minimum_size)

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] == "http" and scope["path"] in GZIP_EXCLUDED_PATHS:
            # Skip GZip for excluded paths - pass through directly
            await self.app(scope, receive, send)
        else:
            # Apply GZip for all other paths
            await self.gzip_app(scope, receive, send)


class RequestIDMiddleware(BaseHTTPMiddleware):
    """
    Middleware to add a unique request ID and timestamp to each request and response.

    This middleware generates a unique request ID and a timestamp when a request is received.
    It adds these values to the request state and includes them in the response headers.


    Methods:
        dispatch(request: Request, call_next):
            Generates a unique request ID and timestamp, adds them to the request state,
            and includes them in the response headers.

    Returns:
        Response: The HTTP response with added request ID and timestamp headers.
    """

    async def dispatch(self, request: Request, call_next):
        """
        Middleware dispatch method to handle incoming requests.

        This method generates a unique request ID and a timestamp for each incoming request,
        adds them to the request state, and includes them in the response headers for
        logging purposes.

        Args:
            request (Request): The incoming HTTP request.
            call_next (Callable): The next middleware or route handler to be called.

        Returns:
            Response: The HTTP response with added headers for request ID and timestamp.
        """
        # Generate request ID and timestamp
        request_id = str(uuid.uuid4())
        timestamp = datetime.now().isoformat()

        # Add request ID and timestamp to request headers
        request.state.request_id = request_id
        request.state.timestamp = timestamp
        logger.debug(f"Requesting: Request ID: {request_id}, Timestamp: {timestamp}")

        # Proceed with the request
        response = await call_next(request)

        # Add request ID and timestamp to response headers for logging
        response.headers["X-Request-ID"] = request_id
        response.headers["X-Timestamp"] = timestamp
        logger.debug(f"Response: Request ID: {request_id}, Timestamp: {timestamp}")

        return response


def setup_middleware(
    app: FastAPI,
    graph_config: GraphConfig | None = None,
    container: InjectQ | None = None,
):
    """
    Set up middleware for the FastAPI application.

    Args:
        app (FastAPI): The FastAPI application instance.
        graph_config (GraphConfig | None): Optional graph configuration used to
            enable dynamic rate limiting from ``agentflow.json``.

    Middleware:
        - CORS: Configured based on settings.ORIGINS.
        - TrustedHost: Configured with allowed hosts from settings.ALLOWED_HOST.
        - GZip: Applied with a minimum size of 1000 bytes (excludes streaming endpoints).
        - RateLimit: Applied when ``rate_limit`` is configured in ``agentflow.json``.
    """
    settings = get_settings()
    # init cors
    app.add_middleware(
        CORSMiddleware,
        allow_origins=settings.ORIGINS.split(","),
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

    app.add_middleware(TrustedHostMiddleware, allowed_hosts=settings.ALLOWED_HOST.split(","))

    # Add request size limit middleware (protects against DoS via large payloads)
    app.add_middleware(RequestSizeLimitMiddleware, max_size=settings.MAX_REQUEST_SIZE)

    # Add security headers middleware (if enabled)
    if settings.SECURITY_HEADERS_ENABLED:
        app.add_middleware(
            SecurityHeadersMiddleware,
            enable_hsts=settings.HSTS_ENABLED,
            hsts_max_age=settings.HSTS_MAX_AGE,
            hsts_include_subdomains=settings.HSTS_INCLUDE_SUBDOMAINS,
            hsts_preload=settings.HSTS_PRELOAD,
            frame_options=settings.FRAME_OPTIONS,
            content_type_options=settings.CONTENT_TYPE_OPTIONS,
            xss_protection=settings.XSS_PROTECTION,
            referrer_policy=settings.REFERRER_POLICY,
            permissions_policy=settings.PERMISSIONS_POLICY,
            csp_policy=settings.CSP_POLICY,
        )

    app.add_middleware(RequestIDMiddleware)

    # Use SelectiveGZipMiddleware to exclude streaming endpoints from compression
    # Streaming endpoints need immediate data transmission without buffering
    app.add_middleware(SelectiveGZipMiddleware, minimum_size=1000)

    # Apply rate limiting only when configured in agentflow.json
    if graph_config is not None:
        rate_limit_config = graph_config.rate_limit
        if rate_limit_config is not None:
            backend = build_backend(rate_limit_config, container=container)
            # Store on app.state so lifespan can close it cleanly
            app.state.rate_limit_backend = backend
            app.add_middleware(
                RateLimitMiddleware,
                config=rate_limit_config,
                backend=backend,
            )
            logger.info(
                "Rate limiting enabled: backend=%s, %d req/%ds, by=%s, "
                "exclude_paths=%s, trusted_proxy_headers=%s",
                rate_limit_config.backend,
                rate_limit_config.requests,
                rate_limit_config.window,
                rate_limit_config.by,
                rate_limit_config.exclude_paths or "(none)",
                rate_limit_config.trusted_proxy_headers,
            )

    logger.debug("Middleware set up")

    # Initialize Sentry
    init_sentry(settings)
