"""
API Monitor Middleware for FastAPI applications.

Automatically captures request/response metrics and sends them to a central
monitoring service in a non-blocking manner.
"""

import os
import asyncio
from datetime import datetime
from typing import Optional, List, Set
from urllib.parse import urlparse

import httpx
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request

# Max request body size to capture (10KB) - prevents memory issues on large uploads
MAX_BODY_CAPTURE_BYTES = 10 * 1024

# HTTP methods that typically have a request body worth capturing
BODY_METHODS = {"POST", "PUT", "PATCH"}


# Default paths to exclude from logging (noise reduction)
DEFAULT_EXCLUDE_PATHS: Set[str] = {
    # Health checks & root probes
    "/",
    "/health",
    "/healthz",
    "/ready",
    "/robots.txt",
    "/favicon.ico",
    "/sitemap.xml",

    # Common vulnerability scanner targets (PHP/Laravel/WordPress)
    "/.env",
    "/wp-login.php",
    "/wp-admin",
    "/xmlrpc.php",
    "/admin",
    "/administrator",
    "/.git/config",
    "/config.php",
    "/phpinfo.php",
    "/info.php",
    "/server-status",
    "/.htaccess",
    "/web.config",
}

# Path prefixes to exclude (matched with startswith)
DEFAULT_EXCLUDE_PREFIXES: tuple = (
    "/vendor/",          # PHP composer paths
    "/_ignition/",       # Laravel debug
    "/api/actions",      # Common scanner probe
    "/_next/",           # Next.js internals
    "/static/",          # Static files (optional)
    "/.well-known/",     # ACME/Let's Encrypt
    "/wp-",              # WordPress paths
    "/js/",              # JS file probes
    "/css/",             # CSS file probes
)


class APIMonitorMiddleware(BaseHTTPMiddleware):
    """
    Middleware to automatically monitor API requests.

    Usage:
        from fastapi import FastAPI
        from tigzig_api_monitor import APIMonitorMiddleware

        app = FastAPI()
        app.add_middleware(APIMonitorMiddleware, app_name="YOUR_APP")

        # RECOMMENDED: Whitelist mode - only log YOUR endpoints (v1.3.0+)
        app.add_middleware(
            APIMonitorMiddleware,
            app_name="YOUR_APP",
            include_prefixes=("/excel/", "/stock/", "/api/"),  # Only log these
        )

        # Alternative: Blacklist mode - exclude specific paths
        app.add_middleware(
            APIMonitorMiddleware,
            app_name="YOUR_APP",
            exclude_paths={"/internal", "/metrics"},
            exclude_prefixes=("/debug/",),
        )

    Environment Variables:
        API_MONITOR_URL: URL of the monitoring service (required)
        API_MONITOR_KEY: API key for authentication (required)
        API_MONITOR_URL_INTERNAL: Optional internal Docker URL (v1.5.0+).
            If set, logs are sent here first. Falls back to API_MONITOR_URL
            on connection failure (sticky — once failed, skips for process lifetime).
    """

    def __init__(
        self,
        app,
        app_name: str,
        include_prefixes: Optional[tuple] = None,
        exclude_paths: Optional[Set[str]] = None,
        exclude_prefixes: Optional[tuple] = None,
        include_noise: bool = False
    ):
        """
        Initialize the middleware.

        Args:
            app: The FastAPI/Starlette application
            app_name: Name to identify this application in logs
            include_prefixes: WHITELIST mode - only log paths starting with these prefixes.
                              If set, exclude_paths/exclude_prefixes are ignored.
                              Example: ("/excel/", "/stock/", "/api/")
            exclude_paths: BLACKLIST mode - exact paths to exclude from logging
            exclude_prefixes: BLACKLIST mode - path prefixes to exclude
            include_noise: If True, log everything (ignore all filters)
        """
        super().__init__(app)
        self.app_name = app_name
        self.monitor_url = os.getenv("API_MONITOR_URL", "")
        self.monitor_url_internal = os.getenv("API_MONITOR_URL_INTERNAL", "")
        self.api_key = os.getenv("API_MONITOR_KEY", "")
        self._internal_failed = False
        self.include_noise = include_noise

        # v1.3.0: Whitelist mode takes priority
        self.include_prefixes = include_prefixes

        # Build exclusion sets (only used if include_prefixes is not set)
        if include_noise:
            self.exclude_paths = set()
            self.exclude_prefixes = ()
        else:
            self.exclude_paths = DEFAULT_EXCLUDE_PATHS.copy()
            if exclude_paths:
                self.exclude_paths.update(exclude_paths)

            self.exclude_prefixes = DEFAULT_EXCLUDE_PREFIXES
            if exclude_prefixes:
                self.exclude_prefixes = DEFAULT_EXCLUDE_PREFIXES + exclude_prefixes

        # Log configuration
        if self.include_prefixes:
            print(f"[APIMonitor] {app_name}: WHITELIST mode - only logging: {self.include_prefixes}")
        elif not include_noise:
            print(f"[APIMonitor] {app_name}: BLACKLIST mode - using default exclusions")

        if not self.monitor_url and not self.monitor_url_internal:
            print(f"[APIMonitor] WARNING: API_MONITOR_URL not set - monitoring disabled for {app_name}")
        if not self.api_key:
            print(f"[APIMonitor] WARNING: API_MONITOR_KEY not set - monitoring disabled for {app_name}")
        if self.monitor_url_internal:
            print(f"[APIMonitor] {app_name}: internal URL configured (Docker network)")

    def _get_client_ip(self, request: Request) -> str:
        """Extract client IP from request headers.

        Priority order for proxy headers:
        1. CF-Connecting-IP (Cloudflare's real client IP header)
        2. X-Real-IP (Nginx and other reverse proxies)
        3. X-Forwarded-For (generic proxy header, take first IP)
        4. request.client.host (direct connection fallback)
        """
        # Cloudflare's header - most reliable when behind CF proxy
        cf_ip = request.headers.get("cf-connecting-ip", "")
        if cf_ip:
            return cf_ip.strip()

        # X-Real-IP (Nginx and other reverse proxies)
        real_ip = request.headers.get("x-real-ip", "")
        if real_ip:
            return real_ip.strip()

        # X-Forwarded-For (generic proxy header, take first IP)
        forwarded = request.headers.get("x-forwarded-for", "")
        if forwarded:
            return forwarded.split(",")[0].strip()

        # Fall back to direct client IP
        if request.client:
            return request.client.host

        return "unknown"

    def _get_referer_path(self, request: Request) -> Optional[str]:
        """Extract path from referer header (no query params for privacy)."""
        referer = request.headers.get("referer", "")
        if referer:
            try:
                parsed = urlparse(referer)
                return parsed.path or "/"
            except Exception:
                pass
        return None

    async def _capture_body(self, request: Request) -> Optional[str]:
        """Capture request body for POST/PUT/PATCH requests.

        Returns body as string (truncated to MAX_BODY_CAPTURE_BYTES) or None.
        Only captures for methods that typically have meaningful bodies.
        """
        if request.method not in BODY_METHODS:
            return None

        try:
            body_bytes = await request.body()
            if not body_bytes:
                return None
            # Truncate and decode, replacing invalid chars
            return body_bytes[:MAX_BODY_CAPTURE_BYTES].decode("utf-8", errors="replace")
        except Exception:
            return None

    def _should_log(self, path: str) -> bool:
        """Check if this path should be logged.

        v1.3.0: Supports two modes:
        - WHITELIST mode (include_prefixes set): Only log paths matching prefixes
        - BLACKLIST mode (default): Log everything except excluded paths
        """
        # v1.3.0: Whitelist mode - only log if path matches include_prefixes
        if self.include_prefixes:
            return path.startswith(self.include_prefixes)

        # Blacklist mode - exclude specific paths
        # Check exact matches
        if path in self.exclude_paths:
            return False

        # Check prefix matches
        if path.startswith(self.exclude_prefixes):
            return False

        return True

    async def _send_log(self, log_data: dict):
        """Send log data to monitoring service (fire-and-forget).

        v1.5.0: Tries internal Docker URL first if configured.
        Falls back to external URL on connection failure (sticky flag).
        """
        if not self.api_key:
            return
        if not self.monitor_url and not self.monitor_url_internal:
            return

        headers = {
            "Content-Type": "application/json",
            "X-API-Key": self.api_key
        }

        # Try internal URL first (Docker network, no SSL)
        if self.monitor_url_internal and not self._internal_failed:
            try:
                async with httpx.AsyncClient(timeout=5.0, verify=False) as client:
                    await client.post(self.monitor_url_internal, json=log_data, headers=headers)
                return
            except httpx.ConnectError:
                self._internal_failed = True
                print(f"[APIMonitor] {self.app_name}: internal URL unreachable, falling back to external")
            except Exception:
                pass

        # Fall back to external URL
        if self.monitor_url:
            try:
                async with httpx.AsyncClient(timeout=5.0) as client:
                    await client.post(self.monitor_url, json=log_data, headers=headers)
            except Exception:
                pass

    async def dispatch(self, request: Request, call_next):
        """Process request and capture metrics."""
        path = request.url.path

        # Check if this path should be logged
        should_log = self._should_log(path)

        if not should_log:
            # Skip logging, just pass through
            return await call_next(request)

        start_time = datetime.now()

        # v1.4.0: Capture request body before call_next (body can only be read once)
        request_body = await self._capture_body(request)

        # Capture request details
        # v1.2.0: Send ALL headers as JSON dict so logger service can extract what it needs
        # This is future-proof - adding new header fields only requires updating logger service
        log_data = {
            "app_name": self.app_name,
            "endpoint": path,  # Path only, no query params
            "method": request.method,
            "client_ip": self._get_client_ip(request),
            "headers": dict(request.headers),  # ALL headers as JSON dict
            "request_body": request_body,  # v1.4.0: Raw body for POST/PUT/PATCH
            "status_code": 500,  # Default, updated after response
            "response_time_ms": 0,
            "error_message": None
        }

        try:
            # Process the request
            response = await call_next(request)
            log_data["status_code"] = response.status_code

        except Exception as e:
            # Capture error details
            log_data["error_message"] = str(e)[:1000]
            raise

        finally:
            # Calculate response time
            end_time = datetime.now()
            log_data["response_time_ms"] = int((end_time - start_time).total_seconds() * 1000)

            # Send log in background (non-blocking)
            asyncio.create_task(self._send_log(log_data))

        return response
