"""Security headers middleware for Flask applications.

This module provides comprehensive HTTP security headers to protect against
common web vulnerabilities identified by OWASP ZAP and other security scanners.
"""

from typing import Optional

from flask import Flask, Response, request
from loguru import logger


class SecurityHeaders:
    """Configure and apply security headers to Flask responses.

    Addresses security vulnerabilities:
    - CSP: Content Security Policy to prevent XSS
    - Clickjacking: X-Frame-Options protection
    - MIME sniffing: X-Content-Type-Options
    - Spectre: Cross-Origin policies
    - Feature abuse: Permissions-Policy
    - Information leakage: Server header removal
    - Protocol downgrade: HSTS (Strict-Transport-Security)
    """

    def __init__(self, app: Optional[Flask] = None) -> None:
        """Initialize security headers middleware.

        Args:
            app (Optional[Flask]): Flask application instance (optional, can call init_app later)
        """
        if app is not None:
            self.init_app(app)

    def init_app(self, app: Flask):
        """Initialize the Flask application with security headers.

        Args:
            app: Flask application instance

        Security Configuration:
            SECURITY_CSP_CONNECT_SRC: Restricts WebSocket/fetch connections (default: 'self')
            SECURITY_CORS_ENABLED: Enable CORS for API routes (default: True)
            SECURITY_CORS_ALLOW_CREDENTIALS: Allow credentials in CORS (default: False)
            SECURITY_CORS_ALLOWED_ORIGINS: Allowed CORS origins, comma-separated for multiple
                                           (default: "" - requires explicit configuration).
                                           Multi-origin uses origin reflection.
            SECURITY_COEP_POLICY: Cross-Origin-Embedder-Policy applied globally to ALL routes
                                  (default: "credentialless"). Options: require-corp, credentialless, unsafe-none

        Important Security Trade-offs:
            - CSP includes 'unsafe-inline' for Socket.IO compatibility
            - This reduces XSS protection but is necessary for real-time WebSocket functionality
            - 'unsafe-eval' has been removed to prevent eval() XSS attacks
            - If Socket.IO is removed, 'unsafe-inline' should also be tightened immediately
            - Monitor CSP violation reports to detect potential XSS attempts
            - COEP policy is applied globally, not per-route. 'credentialless' allows cross-origin
              requests without credentials. Use 'require-corp' for stricter isolation.
        """
        # Set default security configuration
        # connect-src restricted to 'self' — covers same-origin HTTP requests.
        # Socket.IO uses HTTP long-polling transport with same-origin URLs
        # (baseUrl = window.location.protocol + '//' + window.location.host),
        # so explicit ws:/wss: schemes are not needed.
        app.config.setdefault(
            "SECURITY_CSP_CONNECT_SRC",
            "'self'",
        )
        app.config.setdefault("SECURITY_CORS_ENABLED", True)
        app.config.setdefault("SECURITY_CORS_ALLOW_CREDENTIALS", False)

        # CORS allowed origins: check env var first, then fall back to default
        # Env var: LDR_SECURITY_CORS_ALLOWED_ORIGINS
        # Default to empty (fail closed) - require explicit configuration for CORS
        from ..settings.env_registry import get_env_setting

        cors_env = get_env_setting("security.cors.allowed_origins")
        if cors_env is not None:
            app.config["SECURITY_CORS_ALLOWED_ORIGINS"] = cors_env
        else:
            app.config.setdefault("SECURITY_CORS_ALLOWED_ORIGINS", "")

        app.config.setdefault("SECURITY_COEP_POLICY", "credentialless")

        self.app = app

        # Pre-compute static header values once at startup to avoid
        # regenerating identical strings on every response
        self._cached_csp = self.get_csp_policy()
        self._cached_permissions_policy = self.get_permissions_policy()

        app.after_request(self.add_security_headers)

        # Validate CORS configuration at startup
        self._validate_cors_config()

        logger.info("Security headers middleware initialized")
        logger.warning(
            "CSP configured with 'unsafe-inline' for Socket.IO compatibility. "
            "'unsafe-eval' removed for better XSS protection. Monitor for CSP violations."
        )

    def get_csp_policy(self) -> str:
        """Generate Content Security Policy header value.

        Returns:
            CSP policy string with directives for Socket.IO compatibility

        Note:
            - 'unsafe-inline' is required for Socket.IO compatibility; removing it
              would break real-time WebSocket functionality. If Socket.IO is removed
              in the future, 'unsafe-inline' should be tightened immediately.
            - 'unsafe-eval' removed for better XSS protection (not needed for Socket.IO)
            - connect-src defaults to 'self' (same-origin HTTP + WebSocket)
        """
        connect_src = self.app.config.get("SECURITY_CSP_CONNECT_SRC", "'self'")
        return (
            "default-src 'self'; "
            f"connect-src {connect_src}; "
            "script-src 'self' 'unsafe-inline'; "
            "style-src 'self' 'unsafe-inline'; "
            "font-src 'self' data:; "
            "img-src 'self' data:; "
            "media-src 'self'; "
            "worker-src blob:; "
            "child-src 'self' blob:; "
            "frame-src 'self'; "
            "frame-ancestors 'self'; "
            "manifest-src 'self'; "
            "object-src 'none'; "
            "base-uri 'self'; "
            "form-action 'self';"
        )

    @staticmethod
    def get_permissions_policy() -> str:
        """Generate Permissions-Policy header value.

        Disables potentially dangerous browser features by default.

        Returns:
            Permissions-Policy string
        """
        return (
            "geolocation=(), "
            "midi=(), "
            "camera=(), "
            "usb=(), "
            "magnetometer=(), "
            "accelerometer=(), "
            "gyroscope=(), "
            "microphone=(), "
            "payment=(), "
            "sync-xhr=(), "
            "document-domain=()"
        )

    def add_security_headers(self, response: Response) -> Response:
        """Add comprehensive security headers to Flask response.

        Args:
            response: Flask response object

        Returns:
            Response object with security headers added

        Note:
            Cross-Origin-Embedder-Policy (COEP) is applied globally to all routes.
            Default is 'credentialless' which allows cross-origin requests without
            credentials. If stricter isolation is needed, configure SECURITY_COEP_POLICY
            to 'require-corp', but this may break cross-origin API access.
        """
        # Content Security Policy - prevents XSS and injection attacks
        response.headers["Content-Security-Policy"] = self._cached_csp

        # Anti-clickjacking protection
        response.headers["X-Frame-Options"] = "SAMEORIGIN"

        # Prevent MIME-type sniffing
        response.headers["X-Content-Type-Options"] = "nosniff"

        # Spectre vulnerability mitigation - applied globally
        response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
        coep_policy = self.app.config.get(
            "SECURITY_COEP_POLICY", "credentialless"
        )
        response.headers["Cross-Origin-Embedder-Policy"] = coep_policy
        response.headers["Cross-Origin-Resource-Policy"] = "same-origin"

        # Permissions Policy - disable dangerous features
        response.headers["Permissions-Policy"] = self._cached_permissions_policy

        # Remove server version information to prevent information disclosure
        response.headers.pop("Server", None)

        # Referrer Policy - control referrer information
        response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"

        # HSTS - enforce HTTPS connections (only when request is secure)
        # request.is_secure checks wsgi.url_scheme == 'https'
        # ProxyFix (configured in app_factory) handles X-Forwarded-Proto for reverse proxies
        if request.is_secure:
            response.headers["Strict-Transport-Security"] = (
                "max-age=31536000; includeSubDomains"
            )

        # Cache-Control - prevent caching of sensitive content
        # Static assets are handled separately by the static file route
        if not request.path.startswith("/static/"):
            response.headers["Cache-Control"] = (
                "no-store, no-cache, must-revalidate, max-age=0"
            )
            response.headers["Pragma"] = "no-cache"  # HTTP/1.0 compatibility
            response.headers["Expires"] = "0"

        # Add CORS headers for API requests if enabled
        if self._is_api_route(request.path) and self.app.config.get(
            "SECURITY_CORS_ENABLED", True
        ):
            response = self._add_cors_headers(response)

        return response

    @staticmethod
    def _is_api_route(path: str) -> bool:
        """Check if the request path is an API route.

        Args:
            path: Request path to check

        Returns:
            True if path is an API route
        """
        return (
            path.startswith("/api/")
            or path.startswith("/research/api/")
            or path.startswith("/history/api")
        )

    def _validate_cors_config(self) -> None:
        """Validate CORS configuration at startup to catch misconfigurations early.

        Raises:
            ValueError: If CORS configuration is invalid
        """
        if not self.app.config.get("SECURITY_CORS_ENABLED", True):
            return

        allowed_origins = self.app.config.get(
            "SECURITY_CORS_ALLOWED_ORIGINS", ""
        )
        allow_credentials = self.app.config.get(
            "SECURITY_CORS_ALLOW_CREDENTIALS", False
        )

        # Validate credentials with wildcard origin
        if allow_credentials and allowed_origins == "*":
            raise ValueError(
                "CORS misconfiguration: Cannot use credentials with wildcard origin '*'. "
                "Set SECURITY_CORS_ALLOWED_ORIGINS to a specific origin or disable credentials."
            )

        # Log info about multi-origin + credentials configuration
        if allow_credentials and "," in allowed_origins:
            logger.info(
                f"CORS configured with multiple origins and credentials enabled. "
                f"Origins: {allowed_origins}. "
                f"Using origin reflection pattern for security."
            )

    def _add_cors_headers(self, response: Response) -> Response:
        """Add CORS headers for API routes using origin reflection for multi-origin support.

        Args:
            response: Flask response object to modify

        Returns:
            Response object with CORS headers added

        Security Note:
            - Uses "origin reflection" pattern for multi-origin support (comma-separated origins config)
            - Reflects the requesting origin back if it matches the whitelist
            - Wildcard origin (*) disables credentials per CORS spec
            - Single origin or reflected origins allow credentials if configured
        """
        configured_origins = self.app.config.get(
            "SECURITY_CORS_ALLOWED_ORIGINS", ""
        )
        allow_credentials = self.app.config.get(
            "SECURITY_CORS_ALLOW_CREDENTIALS", False
        )

        # Determine which origin to send back
        origin_to_send = configured_origins
        request_origin = request.headers.get("Origin")

        # If wildcard, allow all origins
        if configured_origins == "*":
            origin_to_send = "*"
        # If multiple origins configured (comma-separated), use origin reflection
        elif "," in configured_origins:
            # Parse configured origins into a set
            allowed_origins_set = {
                origin.strip() for origin in configured_origins.split(",")
            }

            # Reflect the request origin if it's in the whitelist (when present)
            if request_origin:
                if request_origin in allowed_origins_set:
                    origin_to_send = request_origin
                else:
                    # Request origin not in whitelist — do NOT emit ACAO header
                    # so the browser blocks the request outright.
                    logger.warning(
                        f"CORS request from non-whitelisted origin: {request_origin}. "
                        f"Allowed origins: {configured_origins}"
                    )
                    return response
        # Single origin configured - always use it (browser enforces CORS)
        else:
            origin_to_send = configured_origins

        response.headers["Access-Control-Allow-Origin"] = origin_to_send

        # When the ACAO value is dynamic (not a fixed wildcard), caches must
        # key on the Origin request header to avoid serving one origin's ACAO
        # to a different origin.
        if origin_to_send != "*":
            existing_vary = response.headers.get("Vary", "")
            if not any(
                v.strip().lower() == "origin"
                for v in existing_vary.split(",")
                if v.strip()
            ):
                if existing_vary:
                    response.headers["Vary"] = f"{existing_vary}, Origin"
                else:
                    response.headers["Vary"] = "Origin"

        response.headers["Access-Control-Allow-Methods"] = (
            "GET, POST, PUT, DELETE, OPTIONS"
        )
        response.headers["Access-Control-Allow-Headers"] = (
            "Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override"
        )

        # Set credentials if configured and not using wildcard
        # Note: Startup validation rejects credentials with wildcard origin
        if allow_credentials and origin_to_send != "*":
            response.headers["Access-Control-Allow-Credentials"] = "true"

        response.headers["Access-Control-Max-Age"] = "3600"

        return response
