"""Enhanced HTTP capture with headers, auth, and call stacks.

Provides comprehensive HTTP request/response capture including:
- Full request and response headers
- Authentication type extraction
- Call stack for every request (not just exceptions)
- OpenTelemetry span correlation
"""

from __future__ import annotations

import re
import traceback
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple

# Import correlation if available
try:
    from autodebug_probe.correlation import get_correlation_id, get_async_task_id
except ImportError:
    def get_correlation_id() -> Optional[str]:
        return None
    def get_async_task_id() -> Optional[str]:
        return None


@dataclass
class CapturedRequest:
    """Captured HTTP request details.

    Attributes:
        method: HTTP method (GET, POST, etc.)
        url: Request URL
        headers: Request headers (sanitized)
        auth_type: Detected authentication type
        auth_scheme: Auth scheme (Bearer, Basic, etc.)
        body_hash: Hash of request body
        content_type: Content-Type header
        correlation_id: Request correlation ID
        call_stack: Stack frames leading to this request
        span_id: OpenTelemetry span ID if available
        trace_id: OpenTelemetry trace ID if available
    """

    method: str
    url: str
    headers: Dict[str, str] = field(default_factory=dict)
    auth_type: Optional[str] = None
    auth_scheme: Optional[str] = None
    body_hash: Optional[str] = None
    content_type: Optional[str] = None
    correlation_id: Optional[str] = None
    call_stack: List[Dict[str, Any]] = field(default_factory=list)
    span_id: Optional[str] = None
    trace_id: Optional[str] = None
    task_id: Optional[str] = None


@dataclass
class CapturedResponse:
    """Captured HTTP response details.

    Attributes:
        status_code: HTTP status code
        headers: Response headers
        body_hash: Hash of response body
        content_type: Response Content-Type
        duration_ms: Request duration in milliseconds
    """

    status_code: int
    headers: Dict[str, str] = field(default_factory=dict)
    body_hash: Optional[str] = None
    content_type: Optional[str] = None
    duration_ms: Optional[float] = None


class AuthExtractor:
    """Extract authentication information from HTTP requests.

    Identifies auth type from Authorization headers and other patterns.
    Masks sensitive values while preserving type information.
    """

    # Common auth header patterns
    AUTH_SCHEMES = {
        "bearer": "Bearer Token (JWT/OAuth)",
        "basic": "Basic Auth",
        "digest": "Digest Auth",
        "apikey": "API Key",
        "api-key": "API Key",
        "token": "Token Auth",
        "aws4-hmac-sha256": "AWS Signature V4",
    }

    # Headers that may contain auth info
    AUTH_HEADERS = [
        "authorization",
        "x-api-key",
        "x-auth-token",
        "x-access-token",
        "api-key",
        "apikey",
    ]

    # Cookie patterns for auth
    COOKIE_AUTH_PATTERNS = [
        r"session[_-]?id",
        r"auth[_-]?token",
        r"access[_-]?token",
        r"jwt",
        r"sid",
    ]

    def extract_auth_type(
        self, headers: Dict[str, str]
    ) -> Tuple[Optional[str], Optional[str]]:
        """Extract authentication type from headers.

        Args:
            headers: HTTP headers dict (case-insensitive keys)

        Returns:
            Tuple of (auth_type, auth_scheme) or (None, None)
        """
        # Normalize headers to lowercase
        normalized = {k.lower(): v for k, v in headers.items()}

        # Check Authorization header
        auth_header = normalized.get("authorization", "")
        if auth_header:
            return self._parse_auth_header(auth_header)

        # Check other auth headers
        for header in self.AUTH_HEADERS:
            if header in normalized and normalized[header]:
                return ("API Key", header)

        # Check cookies for auth patterns
        cookies = normalized.get("cookie", "")
        if cookies:
            auth_cookie = self._check_auth_cookies(cookies)
            if auth_cookie:
                return ("Cookie Auth", auth_cookie)

        return (None, None)

    def _parse_auth_header(self, value: str) -> Tuple[str, str]:
        """Parse Authorization header value.

        Args:
            value: Authorization header value

        Returns:
            Tuple of (auth_type, scheme)
        """
        parts = value.strip().split(None, 1)
        if not parts:
            return ("Unknown", "Unknown")

        scheme = parts[0].lower()
        auth_type = self.AUTH_SCHEMES.get(scheme, f"Custom ({scheme})")

        # Additional JWT detection
        if scheme == "bearer" and len(parts) > 1:
            token = parts[1]
            if self._looks_like_jwt(token):
                auth_type = "JWT Bearer Token"

        return (auth_type, scheme)

    def _looks_like_jwt(self, token: str) -> bool:
        """Check if token looks like a JWT.

        Args:
            token: Token string

        Returns:
            True if it has JWT structure (3 base64 parts)
        """
        parts = token.split(".")
        if len(parts) != 3:
            return False

        # Check each part looks like base64
        base64_pattern = re.compile(r"^[A-Za-z0-9_-]+$")
        return all(base64_pattern.match(p) for p in parts)

    def _check_auth_cookies(self, cookies: str) -> Optional[str]:
        """Check for authentication cookies.

        Args:
            cookies: Cookie header value

        Returns:
            Name of auth cookie if found
        """
        for pattern in self.COOKIE_AUTH_PATTERNS:
            regex = re.compile(pattern, re.IGNORECASE)
            for cookie in cookies.split(";"):
                name = cookie.split("=")[0].strip()
                if regex.search(name):
                    return name
        return None


class HeaderSanitizer:
    """Sanitize headers by masking sensitive values.

    Preserves header names and structure while masking sensitive
    values to prevent credential leakage.
    """

    # Headers to completely mask value
    SENSITIVE_HEADERS = {
        "authorization",
        "x-api-key",
        "x-auth-token",
        "x-access-token",
        "api-key",
        "apikey",
        "cookie",
        "set-cookie",
        "x-csrf-token",
        "x-xsrf-token",
    }

    # Headers to partially mask (show length/type)
    PARTIAL_MASK_HEADERS = {
        "x-request-id",
        "x-correlation-id",
        "x-trace-id",
    }

    def sanitize(
        self, headers: Dict[str, str], include_sensitive: bool = False
    ) -> Dict[str, str]:
        """Sanitize headers for safe storage.

        Args:
            headers: Original headers
            include_sensitive: If True, include masked sensitive headers

        Returns:
            Sanitized headers dict
        """
        result = {}
        for name, value in headers.items():
            lower_name = name.lower()

            if lower_name in self.SENSITIVE_HEADERS:
                if include_sensitive:
                    result[name] = self._mask_value(value)
                # Skip entirely if not including sensitive
            elif lower_name in self.PARTIAL_MASK_HEADERS:
                result[name] = self._partial_mask(value)
            else:
                result[name] = value

        return result

    def _mask_value(self, value: str) -> str:
        """Fully mask a sensitive value.

        Args:
            value: Value to mask

        Returns:
            Masked placeholder
        """
        return f"[MASKED:{len(value)} chars]"

    def _partial_mask(self, value: str) -> str:
        """Partially mask a value, showing prefix.

        Args:
            value: Value to partially mask

        Returns:
            Partially masked value
        """
        if len(value) <= 8:
            return value
        return f"{value[:8]}...[{len(value)} chars]"


class CallStackCapture:
    """Capture call stack for HTTP requests.

    Provides the calling context for each HTTP request to help
    understand where requests originate from.
    """

    # Frames to skip (internal/library frames)
    SKIP_MODULES = {
        "autodebug_probe",
        "requests",
        "httpx",
        "urllib3",
        "http.client",
        "ssl",
        "socket",
    }

    MAX_FRAMES = 15

    def capture(self, skip_frames: int = 0) -> List[Dict[str, Any]]:
        """Capture current call stack.

        Args:
            skip_frames: Additional frames to skip

        Returns:
            List of frame dicts with file, line, function, code
        """
        frames = []
        raw_frames = traceback.extract_stack()

        # Skip our own frames plus requested skip
        start = skip_frames + 2  # Skip capture() and caller

        for frame in raw_frames[:-start][::-1]:  # Reverse order
            # Skip internal modules
            if any(mod in frame.filename for mod in self.SKIP_MODULES):
                continue

            frames.append(
                {
                    "file": self._shorten_path(frame.filename),
                    "line": frame.lineno,
                    "function": frame.name,
                    "code": frame.line,
                }
            )

            if len(frames) >= self.MAX_FRAMES:
                break

        return frames

    def _shorten_path(self, path: str) -> str:
        """Shorten file path for display.

        Args:
            path: Full file path

        Returns:
            Shortened path
        """
        # Try to make path relative to common patterns
        patterns = [
            "/site-packages/",
            "/lib/python",
            "/src/",
            "/app/",
        ]

        for pattern in patterns:
            if pattern in path:
                idx = path.index(pattern)
                return "..." + path[idx:]

        # Return last 3 components
        parts = path.split("/")
        if len(parts) > 3:
            return ".../".join(parts[-3:])

        return path


class EnhancedHTTPCapture:
    """Enhanced HTTP request/response capture.

    Combines auth extraction, header sanitization, and call stacks
    for comprehensive HTTP capture with OpenTelemetry correlation.
    """

    def __init__(
        self,
        include_call_stacks: bool = True,
        include_auth_type: bool = True,
        sanitize_headers: bool = True,
    ):
        """Initialize enhanced HTTP capture.

        Args:
            include_call_stacks: Capture call stacks for requests
            include_auth_type: Extract auth type from headers
            sanitize_headers: Sanitize sensitive header values
        """
        self.include_call_stacks = include_call_stacks
        self.include_auth_type = include_auth_type
        self.sanitize_headers = sanitize_headers

        self.auth_extractor = AuthExtractor()
        self.header_sanitizer = HeaderSanitizer()
        self.stack_capture = CallStackCapture()

    def capture_request(
        self,
        method: str,
        url: str,
        headers: Dict[str, str],
        body_hash: Optional[str] = None,
    ) -> CapturedRequest:
        """Capture an HTTP request with enhanced details.

        Args:
            method: HTTP method
            url: Request URL
            headers: Request headers
            body_hash: Hash of request body

        Returns:
            CapturedRequest with full details
        """
        # Extract auth type
        auth_type = None
        auth_scheme = None
        if self.include_auth_type:
            auth_type, auth_scheme = self.auth_extractor.extract_auth_type(headers)

        # Sanitize headers
        sanitized_headers = headers
        if self.sanitize_headers:
            sanitized_headers = self.header_sanitizer.sanitize(
                headers, include_sensitive=True
            )

        # Capture call stack
        call_stack = []
        if self.include_call_stacks:
            call_stack = self.stack_capture.capture(skip_frames=1)

        # Get correlation ID
        correlation_id = get_correlation_id()
        task_id = get_async_task_id()

        # Get content type
        content_type = None
        for k, v in headers.items():
            if k.lower() == "content-type":
                content_type = v
                break

        # Try to get OpenTelemetry span/trace
        span_id = None
        trace_id = None
        try:
            from opentelemetry import trace

            span = trace.get_current_span()
            if span and span.is_recording():
                ctx = span.get_span_context()
                span_id = format(ctx.span_id, "016x")
                trace_id = format(ctx.trace_id, "032x")
        except ImportError:
            pass

        return CapturedRequest(
            method=method,
            url=url,
            headers=sanitized_headers,
            auth_type=auth_type,
            auth_scheme=auth_scheme,
            body_hash=body_hash,
            content_type=content_type,
            correlation_id=correlation_id,
            call_stack=call_stack,
            span_id=span_id,
            trace_id=trace_id,
            task_id=task_id,
        )

    def capture_response(
        self,
        status_code: int,
        headers: Dict[str, str],
        body_hash: Optional[str] = None,
        duration_ms: Optional[float] = None,
    ) -> CapturedResponse:
        """Capture an HTTP response.

        Args:
            status_code: HTTP status code
            headers: Response headers
            body_hash: Hash of response body
            duration_ms: Request duration

        Returns:
            CapturedResponse with details
        """
        sanitized_headers = headers
        if self.sanitize_headers:
            sanitized_headers = self.header_sanitizer.sanitize(
                headers, include_sensitive=False
            )

        content_type = None
        for k, v in headers.items():
            if k.lower() == "content-type":
                content_type = v
                break

        return CapturedResponse(
            status_code=status_code,
            headers=sanitized_headers,
            body_hash=body_hash,
            content_type=content_type,
            duration_ms=duration_ms,
        )

    def to_payload(
        self, request: CapturedRequest, response: Optional[CapturedResponse] = None
    ) -> Dict[str, Any]:
        """Convert captured request/response to recording payload.

        Args:
            request: Captured request
            response: Optional captured response

        Returns:
            Dict payload for recording
        """
        payload: Dict[str, Any] = {
            "method": request.method,
            "url": request.url,
            "request_headers": request.headers,
            "body_hash": request.body_hash,
            "content_type": request.content_type,
        }

        if request.auth_type:
            payload["auth_type"] = request.auth_type
            payload["auth_scheme"] = request.auth_scheme

        if request.correlation_id:
            payload["correlation_id"] = request.correlation_id

        if request.call_stack:
            payload["call_stack"] = request.call_stack

        if request.span_id:
            payload["span_id"] = request.span_id
            payload["trace_id"] = request.trace_id
        if request.task_id:
            payload["task_id"] = request.task_id

        if response:
            payload["status"] = response.status_code
            payload["response_headers"] = response.headers
            payload["response_body_hash"] = response.body_hash
            payload["response_content_type"] = response.content_type
            if response.duration_ms:
                payload["duration_ms"] = response.duration_ms

        return payload


# Singleton instance for default capture
_default_capture: Optional[EnhancedHTTPCapture] = None


def get_http_capture() -> EnhancedHTTPCapture:
    """Get the default HTTP capture instance.

    Returns:
        EnhancedHTTPCapture singleton
    """
    global _default_capture
    if _default_capture is None:
        _default_capture = EnhancedHTTPCapture()
    return _default_capture
