"""FastAPI middleware for credential extraction and validation.

This module provides middleware for FastAPI applications to:
- Extract and validate API key from Authorization header (Bearer token)
- Extract and validate MetaInfo JWT from X-META-INFO header

Note: This middleware only extracts and validates credentials. Authorization decisions
are left to route handlers.
"""

import logging
from pathlib import Path

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

from hezor_common.security.jwt import decode_jwt_with_pem
from hezor_common.transfer.base_sdk.constants import ANONYMOUS_HEADER_PUBLIC_KEY
from hezor_common.transfer.base_sdk.meta_info import MetaInfo

logger = logging.getLogger(__name__)


class CredentialMiddleware(BaseHTTPMiddleware):
    """Credential extraction and validation middleware for FastAPI.

    This middleware extracts and validates credentials:
    - API key from Authorization header (Bearer token)
    - MetaInfo from X-META-INFO header (JWT token)

    The validated credentials are stored in request.state for authorization decisions
    in route handlers.

    Attributes
    ----------
    api_key : str | None
        Expected API key for validation. If None, API key validation is skipped.
    public_key_pem : str | bytes | None
        PEM-encoded public key for JWT verification. If None, uses anonymous public key.
    public_key_path : str | Path | None
        Path to public key file. Takes precedence over public_key_pem if provided.
    verify_jwt : bool
        Whether to verify JWT signature (default: True)

    Examples
    --------
    >>> from fastapi import FastAPI
    >>> app = FastAPI()
    >>> # Using public key content
    >>> app.add_middleware(
    ...     CredentialMiddleware,
    ...     api_key="your-api-key",
    ...     public_key_pem=public_key_content,
    ... )
    >>> # Using public key file path
    >>> app.add_middleware(
    ...     CredentialMiddleware,
    ...     api_key="your-api-key",
    ...     public_key_path="/path/to/public_key.pem",
    ... )
    """

    def __init__(
        self,
        app,
        api_key: str | None = None,
        public_key_pem: str | bytes | None = None,
        public_key_path: str | Path | None = None,
        verify_jwt: bool = True,
    ):
        """Initialize credential extraction middleware.

        Parameters
        ----------
        app
            FastAPI application instance
        api_key : str | None, optional
            Expected API key for validation (default: None, accepts any token)
        public_key_pem : str | bytes | None, optional
            PEM-encoded public key for JWT verification (default: None, uses anonymous key)
        public_key_path : str | Path | None, optional
            Path to public key file for JWT verification. Takes precedence over public_key_pem.
        verify_jwt : bool, optional
            Whether to verify JWT signature (default: True)
        """
        super().__init__(app)
        self.api_key = api_key

        # Load public key from file path if provided, otherwise use PEM content
        if public_key_path is not None:
            public_key_file = Path(public_key_path)
            if not public_key_file.exists():
                msg = f"Public key file not found: {public_key_path}"
                raise FileNotFoundError(msg)
            self.public_key_pem = public_key_file.read_bytes()
            logger.info(f"Loaded public key from {public_key_path}")
        else:
            self.public_key_pem = public_key_pem or ANONYMOUS_HEADER_PUBLIC_KEY
            if public_key_pem is None:
                logger.warning(
                    "No public key provided, using anonymous public key. "
                    "This is not secure for production use."
                )

        self.verify_jwt = verify_jwt

    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
        """Process request to extract and validate credentials.

        Extracts and validates:
        1. API key from Authorization header (Bearer token)
        2. MetaInfo from X-META-INFO header (JWT token)

        Stores results in request.state:
        - request.state.api_key: str | None
        - request.state.meta_info: MetaInfo | None
        - request.state.api_key_error: str | None (if API key validation fails)
        - request.state.meta_info_error: str | None (if MetaInfo validation fails)

        Parameters
        ----------
        request : Request
            Incoming HTTP request
        call_next : Callable
            Next middleware or route handler

        Returns
        -------
        Response
            HTTP response
        """
        # Initialize request state
        request.state.api_key = None
        request.state.meta_info = None
        request.state.api_key_error = None
        request.state.meta_info_error = None

        # Extract and validate API key
        auth_header = request.headers.get("Authorization")
        if auth_header:
            if auth_header.startswith("Bearer "):
                token = auth_header[7:]
                if self.api_key is not None:
                    if token == self.api_key:
                        request.state.api_key = token
                        logger.debug(f"API key validated: {token[:6]}***")
                    else:
                        request.state.api_key_error = "Invalid API key"
                        logger.warning("API key validation failed")
                else:
                    # No API key configured, accept any token
                    request.state.api_key = token
                    logger.debug(f"API key accepted (no validation): {token[:6]}***")
            else:
                request.state.api_key_error = "Invalid Authorization header format"
                logger.warning("Invalid Authorization header format")

        # Extract and validate MetaInfo JWT
        meta_header = request.headers.get("X-META-INFO")
        if meta_header:
            try:
                # Convert public key to bytes if needed
                public_key_bytes = (
                    self.public_key_pem.encode("utf-8")
                    if isinstance(self.public_key_pem, str)
                    else self.public_key_pem
                )

                # Decode JWT
                payload = decode_jwt_with_pem(
                    public_key_bytes,
                    meta_header,
                    verify=self.verify_jwt,
                )

                # Parse as MetaInfo
                meta_info = MetaInfo.model_validate(payload)
                request.state.meta_info = meta_info
                logger.debug(
                    f"MetaInfo extracted: subject={meta_info.subject}, "
                    f"subject_code={meta_info.subject_code}"
                )

            except Exception as e:
                request.state.meta_info_error = f"Invalid MetaInfo JWT: {e!s}"
                logger.warning(f"MetaInfo validation failed: {e}")

        # Continue to next middleware/handler
        response = await call_next(request)
        return response


def require_api_key(request: Request) -> str:
    """Dependency to extract and require valid API key.

    Extracts API key from request.state and raises HTTPException if missing or invalid.
    Use this in route handlers that require API key authentication.

    Parameters
    ----------
    request : Request
        FastAPI request object

    Returns
    -------
    str
        Validated API key

    Raises
    ------
    HTTPException
        If API key is missing or invalid

    Examples
    --------
    >>> from fastapi import Depends
    >>> @app.get("/protected")
    >>> async def protected_route(api_key: str = Depends(require_api_key)):
    ...     return {"message": "Access granted"}
    """
    from fastapi import HTTPException

    if hasattr(request.state, "api_key_error") and request.state.api_key_error:
        raise HTTPException(status_code=401, detail=request.state.api_key_error)

    if not hasattr(request.state, "api_key") or request.state.api_key is None:
        raise HTTPException(status_code=401, detail="API key required")

    return request.state.api_key


def require_meta_info(request: Request) -> MetaInfo:
    """Dependency to extract and require valid MetaInfo.

    Extracts MetaInfo from request.state and raises HTTPException if missing or invalid.
    Use this in route handlers that require MetaInfo-based identification.

    Parameters
    ----------
    request : Request
        FastAPI request object

    Returns
    -------
    MetaInfo
        Validated MetaInfo object

    Raises
    ------
    HTTPException
        If MetaInfo is missing or invalid

    Examples
    --------
    >>> from fastapi import Depends
    >>> @app.get("/protected")
    >>> async def protected_route(meta: MetaInfo = Depends(require_meta_info)):
    ...     return {"subject": meta.subject, "subject_code": meta.subject_code}
    """
    from fastapi import HTTPException

    if hasattr(request.state, "meta_info_error") and request.state.meta_info_error:
        raise HTTPException(status_code=401, detail=request.state.meta_info_error)

    if not hasattr(request.state, "meta_info") or request.state.meta_info is None:
        raise HTTPException(status_code=401, detail="MetaInfo required")

    return request.state.meta_info


def get_meta_info(request: Request) -> MetaInfo | None:
    """Dependency to optionally extract MetaInfo.

    Extracts MetaInfo from request.state if present, returns None otherwise.
    Use this in route handlers where MetaInfo is optional.

    Parameters
    ----------
    request : Request
        FastAPI request object

    Returns
    -------
    MetaInfo | None
        MetaInfo object if present and valid, None otherwise

    Examples
    --------
    >>> from fastapi import Depends
    >>> @app.get("/optional")
    >>> async def optional_route(meta: MetaInfo | None = Depends(get_meta_info)):
    ...     if meta:
    ...         return {"subject": meta.subject}
    ...     return {"message": "No MetaInfo provided"}
    """
    if hasattr(request.state, "meta_info"):
        return request.state.meta_info
    return None


def require_credentials(request: Request) -> tuple[str | None, MetaInfo | None]:
    """Dependency to extract and require at least one valid credential.

    Extracts both API key and MetaInfo from request.state. Requires at least one to be valid.
    Use this in route handlers that accept either credential type.

    Parameters
    ----------
    request : Request
        FastAPI request object

    Returns
    -------
    tuple[str | None, MetaInfo | None]
        Tuple of (api_key, meta_info) where at least one is not None

    Raises
    ------
    HTTPException
        If both credentials are missing or invalid

    Examples
    --------
    >>> from fastapi import Depends
    >>> @app.get("/flexible")
    >>> async def flexible_route(cred: tuple = Depends(require_credentials)):
    ...     api_key, meta_info = cred
    ...     if api_key:
    ...         return {"credential_type": "api_key"}
    ...     return {"credential_type": "meta_info", "subject": meta_info.subject}
    """
    from fastapi import HTTPException

    api_key = request.state.api_key if hasattr(request.state, "api_key") else None
    meta_info = request.state.meta_info if hasattr(request.state, "meta_info") else None

    # Check if at least one authentication method succeeded
    if api_key is None and meta_info is None:
        # Collect all errors
        errors = []
        if hasattr(request.state, "api_key_error") and request.state.api_key_error:
            errors.append(f"API key: {request.state.api_key_error}")
        if hasattr(request.state, "meta_info_error") and request.state.meta_info_error:
            errors.append(f"MetaInfo: {request.state.meta_info_error}")

        if errors:
            detail = "Credential validation failed: " + "; ".join(errors)
        else:
            detail = "Valid credential required (API key or MetaInfo)"

        raise HTTPException(status_code=401, detail=detail)

    return api_key, meta_info


def get_credentials(request: Request) -> tuple[str | None, MetaInfo | None]:
    """Dependency to optionally extract credentials.

    Extracts both API key and MetaInfo from request.state if present.
    Use this in route handlers where credentials are optional.

    Parameters
    ----------
    request : Request
        FastAPI request object

    Returns
    -------
    tuple[str | None, MetaInfo | None]
        Tuple of (api_key, meta_info), both may be None

    Examples
    --------
    >>> from fastapi import Depends
    >>> @app.get("/flexible")
    >>> async def flexible_route(cred: tuple = Depends(get_credentials)):
    ...     api_key, meta_info = cred
    ...     if api_key or meta_info:
    ...         return {"authenticated": True}
    ...     return {"authenticated": False}
    """
    api_key = request.state.api_key if hasattr(request.state, "api_key") else None
    meta_info = request.state.meta_info if hasattr(request.state, "meta_info") else None
    return api_key, meta_info
