"""Lightweight production error capture for Hikaflow.

This module provides minimal-overhead I/O capture for production environments.
Unlike test mode which captures everything, prod mode:
- Uses a ring buffer (last N operations only)
- Only uploads context when an error occurs
- Supports sampling for high-traffic services
- Integrates with OpenTelemetry for context propagation

Usage:
    from autodebug_probe.prod_mode import init_prod_capture, capture_exception

    # At app startup
    init_prod_capture(
        api_key="your-key",
        sample_rate=0.1,  # Capture 10% of requests
        max_breadcrumbs=100,
    )

    # In error handlers
    @app.exception_handler(Exception)
    async def handle_error(request, exc):
        error_id = capture_exception(exc)
        return JSONResponse({"error_id": error_id}, status_code=500)
"""

from __future__ import annotations

import atexit
import hashlib
import json
import os
import random
import sys
import threading
import traceback
import uuid
from collections import deque
from contextlib import contextmanager
from contextvars import ContextVar
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from functools import wraps
from pathlib import Path
from typing import Any, Callable, Deque, Dict, List, Optional, Set, TypeVar, Union
import gzip
import io

# Optional imports
try:
    import requests as requests_lib
    HAS_REQUESTS = True
except ImportError:
    HAS_REQUESTS = False

try:
    from opentelemetry import trace as otel_trace
    from opentelemetry.trace import get_current_span, SpanKind
    HAS_OTEL = True
except ImportError:
    HAS_OTEL = False


# Type definitions
F = TypeVar("F", bound=Callable[..., Any])


@dataclass
class Breadcrumb:
    """A single captured I/O operation (breadcrumb)."""

    seq: int
    type: str  # HTTP, SQL, REDIS, MONGODB, GRPC, LOG, CUSTOM
    timestamp: float  # Unix timestamp in ms
    duration_ms: float
    category: str  # e.g., "requests", "psycopg2", "redis"
    message: str  # Human-readable summary
    data: Dict[str, Any] = field(default_factory=dict)
    level: str = "info"  # debug, info, warning, error

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)


@dataclass
class RequestContext:
    """Context for a single request/transaction."""

    request_id: str
    trace_id: Optional[str]
    span_id: Optional[str]
    started_at: float
    breadcrumbs: Deque[Breadcrumb]
    tags: Dict[str, str]
    extra: Dict[str, Any]
    user_id: Optional[str] = None
    user_email: Optional[str] = None
    url: Optional[str] = None
    method: Optional[str] = None

    def to_dict(self) -> Dict[str, Any]:
        return {
            "request_id": self.request_id,
            "trace_id": self.trace_id,
            "span_id": self.span_id,
            "started_at": self.started_at,
            "breadcrumbs": [b.to_dict() for b in self.breadcrumbs],
            "tags": self.tags,
            "extra": self.extra,
            "user_id": self.user_id,
            "user_email": self.user_email,
            "url": self.url,
            "method": self.method,
        }


@dataclass
class ErrorContext:
    """Complete context captured when an error occurs."""

    error_id: str
    timestamp: float
    exception_type: str
    exception_message: str
    exception_stack: List[Dict[str, Any]]
    request_context: RequestContext
    environment: Dict[str, str]
    runtime_info: Dict[str, Any]

    def to_dict(self) -> Dict[str, Any]:
        return {
            "error_id": self.error_id,
            "timestamp": self.timestamp,
            "exception": {
                "type": self.exception_type,
                "message": self.exception_message,
                "stack": self.exception_stack,
            },
            "request": self.request_context.to_dict(),
            "environment": self.environment,
            "runtime": self.runtime_info,
        }


@dataclass
class ProdConfig:
    """Configuration for production capture."""

    api_key: Optional[str] = None
    api_endpoint: str = "https://get.hikaflow.com"
    project_id: Optional[str] = None
    environment: str = "production"
    release: Optional[str] = None
    sample_rate: float = 1.0  # 1.0 = 100% of errors captured
    max_breadcrumbs: int = 100
    capture_request_body: bool = False  # Privacy-sensitive
    capture_response_body: bool = False
    max_body_size: int = 10 * 1024  # 10KB
    redact_patterns: List[str] = field(default_factory=lambda: [
        "password", "secret", "token", "api_key", "apikey",
        "authorization", "cookie", "session", "credential",
    ])
    enabled: bool = True
    debug: bool = False
    local_storage_path: Optional[str] = None  # For offline/local capture
    before_send: Optional[Callable[[ErrorContext], Optional[ErrorContext]]] = None


# Global state
_config: Optional[ProdConfig] = None
_initialized = False
_seq_counter = 0
_seq_lock = threading.Lock()

# Context variable for request-scoped data
_request_context: ContextVar[Optional[RequestContext]] = ContextVar(
    "hikaflow_request_context", default=None
)


def _next_seq() -> int:
    """Get next sequence number (thread-safe)."""
    global _seq_counter
    with _seq_lock:
        _seq_counter += 1
        return _seq_counter


def _now_ms() -> float:
    """Current time in milliseconds."""
    return datetime.now(timezone.utc).timestamp() * 1000


def _redact_value(key: str, value: Any, patterns: List[str]) -> Any:
    """Redact sensitive values based on key patterns."""
    if not isinstance(key, str):
        return value

    key_lower = key.lower()
    for pattern in patterns:
        if pattern in key_lower:
            return "[REDACTED]"

    return value


def _redact_dict(data: Dict[str, Any], patterns: List[str]) -> Dict[str, Any]:
    """Recursively redact sensitive values in a dictionary."""
    result = {}
    for key, value in data.items():
        if isinstance(value, dict):
            result[key] = _redact_dict(value, patterns)
        elif isinstance(value, list):
            result[key] = [
                _redact_dict(v, patterns) if isinstance(v, dict) else v
                for v in value
            ]
        else:
            result[key] = _redact_value(key, value, patterns)
    return result


def _extract_stack(exc: BaseException) -> List[Dict[str, Any]]:
    """Extract stack trace from exception."""
    frames = []
    tb = exc.__traceback__
    while tb is not None:
        frame = tb.tb_frame
        frames.append({
            "file": frame.f_code.co_filename,
            "line": tb.tb_lineno,
            "function": frame.f_code.co_name,
            "locals": {
                k: repr(v)[:200] for k, v in frame.f_locals.items()
                if not k.startswith("_") and not callable(v)
            },
        })
        tb = tb.tb_next

    # Return in reverse order (most recent first)
    return list(reversed(frames))


def _get_otel_context() -> tuple[Optional[str], Optional[str]]:
    """Get OpenTelemetry trace and span IDs if available."""
    if not HAS_OTEL:
        return None, None

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

    return None, None


def _get_runtime_info() -> Dict[str, Any]:
    """Get runtime/environment information."""
    return {
        "python_version": sys.version,
        "platform": sys.platform,
        "pid": os.getpid(),
        "cwd": os.getcwd(),
    }


def _get_safe_env() -> Dict[str, str]:
    """Get environment variables with sensitive ones redacted."""
    if not _config:
        return {}

    safe_env = {}
    for key, value in os.environ.items():
        safe_env[key] = _redact_value(key, value, _config.redact_patterns)

    return safe_env


# =============================================================================
# Public API
# =============================================================================

def init_prod_capture(
    api_key: Optional[str] = None,
    api_endpoint: str = "https://get.hikaflow.com",
    project_id: Optional[str] = None,
    environment: str = "production",
    release: Optional[str] = None,
    sample_rate: float = 1.0,
    max_breadcrumbs: int = 100,
    capture_request_body: bool = False,
    capture_response_body: bool = False,
    redact_patterns: Optional[List[str]] = None,
    debug: bool = False,
    local_storage_path: Optional[str] = None,
    before_send: Optional[Callable[[ErrorContext], Optional[ErrorContext]]] = None,
) -> None:
    """Initialize production capture.

    Args:
        api_key: Hikaflow API key (or set HIKAFLOW_API_KEY env var)
        api_endpoint: API endpoint URL
        project_id: Project ID (or set HIKAFLOW_PROJECT_ID env var)
        environment: Environment name (production, staging, etc.)
        release: Release/version identifier
        sample_rate: Fraction of errors to capture (0.0 to 1.0)
        max_breadcrumbs: Maximum breadcrumbs to keep in ring buffer
        capture_request_body: Whether to capture HTTP request bodies
        capture_response_body: Whether to capture HTTP response bodies
        redact_patterns: Additional patterns to redact (case-insensitive)
        debug: Enable debug logging
        local_storage_path: Path to store errors locally (for offline/testing)
        before_send: Callback to modify/filter errors before sending
    """
    global _config, _initialized

    # Build redact patterns
    default_patterns = [
        "password", "secret", "token", "api_key", "apikey",
        "authorization", "cookie", "session", "credential",
    ]
    all_patterns = default_patterns + (redact_patterns or [])

    _config = ProdConfig(
        api_key=api_key or os.environ.get("HIKAFLOW_API_KEY"),
        api_endpoint=api_endpoint or os.environ.get("HIKAFLOW_API_ENDPOINT", "https://get.hikaflow.com"),
        project_id=project_id or os.environ.get("HIKAFLOW_PROJECT_ID"),
        environment=environment,
        release=release or os.environ.get("HIKAFLOW_RELEASE"),
        sample_rate=sample_rate,
        max_breadcrumbs=max_breadcrumbs,
        capture_request_body=capture_request_body,
        capture_response_body=capture_response_body,
        redact_patterns=all_patterns,
        debug=debug,
        local_storage_path=local_storage_path,
        before_send=before_send,
    )

    # Install patches
    _install_patches()

    _initialized = True

    if debug:
        print(f"[hikaflow] Production capture initialized (sample_rate={sample_rate})")


def start_request(
    request_id: Optional[str] = None,
    url: Optional[str] = None,
    method: Optional[str] = None,
    user_id: Optional[str] = None,
    user_email: Optional[str] = None,
    tags: Optional[Dict[str, str]] = None,
) -> RequestContext:
    """Start a new request context.

    Call this at the beginning of each request/transaction.
    The context is stored in a context variable and automatically
    cleaned up when the request ends.

    Args:
        request_id: Unique request identifier (auto-generated if not provided)
        url: Request URL
        method: HTTP method
        user_id: User identifier
        user_email: User email
        tags: Additional tags

    Returns:
        RequestContext that can be used with `with` statement
    """
    if not _config:
        raise RuntimeError("Production capture not initialized. Call init_prod_capture() first.")

    trace_id, span_id = _get_otel_context()

    ctx = RequestContext(
        request_id=request_id or str(uuid.uuid4()),
        trace_id=trace_id,
        span_id=span_id,
        started_at=_now_ms(),
        breadcrumbs=deque(maxlen=_config.max_breadcrumbs),
        tags=tags or {},
        extra={},
        user_id=user_id,
        user_email=user_email,
        url=url,
        method=method,
    )

    _request_context.set(ctx)
    return ctx


def end_request() -> None:
    """End the current request context."""
    _request_context.set(None)


@contextmanager
def request_context(
    request_id: Optional[str] = None,
    url: Optional[str] = None,
    method: Optional[str] = None,
    user_id: Optional[str] = None,
    user_email: Optional[str] = None,
    tags: Optional[Dict[str, str]] = None,
):
    """Context manager for request lifecycle.

    Usage:
        with request_context(url="/api/users", method="GET"):
            # Handle request
            pass
    """
    ctx = start_request(
        request_id=request_id,
        url=url,
        method=method,
        user_id=user_id,
        user_email=user_email,
        tags=tags,
    )
    try:
        yield ctx
    finally:
        end_request()


def get_current_context() -> Optional[RequestContext]:
    """Get the current request context."""
    return _request_context.get()


def add_breadcrumb(
    type: str,
    category: str,
    message: str,
    data: Optional[Dict[str, Any]] = None,
    level: str = "info",
    duration_ms: float = 0,
) -> None:
    """Add a breadcrumb to the current request context.

    Args:
        type: Breadcrumb type (HTTP, SQL, REDIS, MONGODB, GRPC, LOG, CUSTOM)
        category: Category (e.g., "requests", "psycopg2")
        message: Human-readable summary
        data: Additional structured data
        level: Log level (debug, info, warning, error)
        duration_ms: Operation duration in milliseconds
    """
    ctx = _request_context.get()
    if not ctx or not _config:
        return

    # Redact sensitive data
    safe_data = _redact_dict(data or {}, _config.redact_patterns)

    breadcrumb = Breadcrumb(
        seq=_next_seq(),
        type=type,
        timestamp=_now_ms(),
        duration_ms=duration_ms,
        category=category,
        message=message[:500],  # Limit message length
        data=safe_data,
        level=level,
    )

    ctx.breadcrumbs.append(breadcrumb)

    if _config.debug:
        print(f"[hikaflow] Breadcrumb: {type} - {message[:50]}")


def set_user(user_id: Optional[str] = None, email: Optional[str] = None) -> None:
    """Set user information on the current context."""
    ctx = _request_context.get()
    if ctx:
        if user_id:
            ctx.user_id = user_id
        if email:
            ctx.user_email = email


def set_tag(key: str, value: str) -> None:
    """Set a tag on the current context."""
    ctx = _request_context.get()
    if ctx:
        ctx.tags[key] = value


def set_extra(key: str, value: Any) -> None:
    """Set extra data on the current context."""
    ctx = _request_context.get()
    if ctx and _config:
        ctx.extra[key] = _redact_value(key, value, _config.redact_patterns)


def capture_exception(
    exc: Optional[BaseException] = None,
    tags: Optional[Dict[str, str]] = None,
    extra: Optional[Dict[str, Any]] = None,
) -> Optional[str]:
    """Capture an exception and upload context.

    Args:
        exc: Exception to capture (uses sys.exc_info() if not provided)
        tags: Additional tags
        extra: Additional context data

    Returns:
        Error ID if captured, None if sampled out or disabled
    """
    if not _config or not _config.enabled:
        return None

    # Get exception from sys.exc_info() if not provided
    if exc is None:
        exc_info = sys.exc_info()
        if exc_info[1] is not None:
            exc = exc_info[1]
        else:
            return None

    # Apply sampling
    if _config.sample_rate < 1.0 and random.random() > _config.sample_rate:
        if _config.debug:
            print("[hikaflow] Error sampled out")
        return None

    # Get or create request context
    ctx = _request_context.get()
    if not ctx:
        # Create minimal context for errors outside request scope
        ctx = RequestContext(
            request_id=str(uuid.uuid4()),
            trace_id=None,
            span_id=None,
            started_at=_now_ms(),
            breadcrumbs=deque(maxlen=_config.max_breadcrumbs),
            tags={},
            extra={},
        )

    # Merge additional tags/extra
    if tags:
        ctx.tags.update(tags)
    if extra:
        for k, v in extra.items():
            ctx.extra[k] = _redact_value(k, v, _config.redact_patterns)

    # Build error context
    error_id = str(uuid.uuid4())
    error_context = ErrorContext(
        error_id=error_id,
        timestamp=_now_ms(),
        exception_type=type(exc).__name__,
        exception_message=str(exc)[:2000],
        exception_stack=_extract_stack(exc),
        request_context=ctx,
        environment=_get_safe_env(),
        runtime_info=_get_runtime_info(),
    )

    # Apply before_send hook
    if _config.before_send:
        error_context = _config.before_send(error_context)
        if error_context is None:
            if _config.debug:
                print("[hikaflow] Error dropped by before_send hook")
            return None

    # Upload or store locally
    _upload_error(error_context)

    return error_id


def capture_message(
    message: str,
    level: str = "info",
    tags: Optional[Dict[str, str]] = None,
    extra: Optional[Dict[str, Any]] = None,
) -> Optional[str]:
    """Capture a message (non-exception event).

    Args:
        message: Message to capture
        level: Log level
        tags: Additional tags
        extra: Additional context data

    Returns:
        Error ID if captured
    """
    if not _config or not _config.enabled:
        return None

    # Create a synthetic exception-like context
    ctx = _request_context.get()
    if not ctx:
        ctx = RequestContext(
            request_id=str(uuid.uuid4()),
            trace_id=None,
            span_id=None,
            started_at=_now_ms(),
            breadcrumbs=deque(maxlen=_config.max_breadcrumbs),
            tags={},
            extra={},
        )

    if tags:
        ctx.tags.update(tags)
    if extra:
        for k, v in extra.items():
            ctx.extra[k] = _redact_value(k, v, _config.redact_patterns)

    error_id = str(uuid.uuid4())
    error_context = ErrorContext(
        error_id=error_id,
        timestamp=_now_ms(),
        exception_type="Message",
        exception_message=message[:2000],
        exception_stack=[],
        request_context=ctx,
        environment=_get_safe_env(),
        runtime_info=_get_runtime_info(),
    )

    _upload_error(error_context)
    return error_id


# =============================================================================
# Framework Integrations
# =============================================================================

def wsgi_middleware(app: Callable) -> Callable:
    """WSGI middleware for automatic request tracking.

    Usage:
        app = wsgi_middleware(app)
    """
    def middleware(environ, start_response):
        request_id = environ.get("HTTP_X_REQUEST_ID", str(uuid.uuid4()))
        url = environ.get("PATH_INFO", "/")
        method = environ.get("REQUEST_METHOD", "GET")

        with request_context(request_id=request_id, url=url, method=method):
            try:
                return app(environ, start_response)
            except Exception as exc:
                capture_exception(exc)
                raise

    return middleware


def asgi_middleware(app: Callable) -> Callable:
    """ASGI middleware for automatic request tracking.

    Usage:
        app = asgi_middleware(app)
    """
    async def middleware(scope, receive, send):
        if scope["type"] != "http":
            return await app(scope, receive, send)

        request_id = None
        for header_name, header_value in scope.get("headers", []):
            if header_name == b"x-request-id":
                request_id = header_value.decode()
                break

        url = scope.get("path", "/")
        method = scope.get("method", "GET")

        with request_context(request_id=request_id, url=url, method=method):
            try:
                return await app(scope, receive, send)
            except Exception as exc:
                capture_exception(exc)
                raise

    return middleware


def flask_integration(app) -> None:
    """Integrate with Flask application.

    Usage:
        from flask import Flask
        app = Flask(__name__)
        flask_integration(app)
    """
    @app.before_request
    def before_request():
        from flask import request, g
        g.hikaflow_ctx = start_request(
            request_id=request.headers.get("X-Request-ID"),
            url=request.path,
            method=request.method,
        )

    @app.after_request
    def after_request(response):
        end_request()
        return response

    @app.errorhandler(Exception)
    def handle_exception(exc):
        capture_exception(exc)
        raise exc


def fastapi_integration(app) -> None:
    """Integrate with FastAPI application.

    Usage:
        from fastapi import FastAPI
        app = FastAPI()
        fastapi_integration(app)
    """
    from starlette.middleware.base import BaseHTTPMiddleware
    from starlette.requests import Request

    class HikaflowMiddleware(BaseHTTPMiddleware):
        async def dispatch(self, request: Request, call_next):
            with request_context(
                request_id=request.headers.get("x-request-id"),
                url=str(request.url.path),
                method=request.method,
            ):
                try:
                    response = await call_next(request)
                    return response
                except Exception as exc:
                    capture_exception(exc)
                    raise

    app.add_middleware(HikaflowMiddleware)


# =============================================================================
# Auto-patching for breadcrumb capture
# =============================================================================

_original_functions: Dict[str, Any] = {}


def _install_patches() -> None:
    """Install patches for automatic breadcrumb capture."""
    _patch_requests()
    _patch_httpx()
    _patch_psycopg2()
    _patch_redis()
    _patch_logging()


def _patch_requests() -> None:
    """Patch requests library for HTTP breadcrumbs."""
    try:
        import requests

        if "requests.Session.request" in _original_functions:
            return

        original = requests.Session.request
        _original_functions["requests.Session.request"] = original

        def patched_request(self, method, url, **kwargs):
            start = _now_ms()
            try:
                response = original(self, method, url, **kwargs)
                duration = _now_ms() - start

                add_breadcrumb(
                    type="HTTP",
                    category="requests",
                    message=f"{method} {url} [{response.status_code}]",
                    data={
                        "method": method,
                        "url": url[:500],
                        "status_code": response.status_code,
                    },
                    duration_ms=duration,
                    level="info" if response.status_code < 400 else "error",
                )

                return response
            except Exception as exc:
                duration = _now_ms() - start
                add_breadcrumb(
                    type="HTTP",
                    category="requests",
                    message=f"{method} {url} [ERROR: {type(exc).__name__}]",
                    data={"method": method, "url": url[:500], "error": str(exc)[:200]},
                    duration_ms=duration,
                    level="error",
                )
                raise

        requests.Session.request = patched_request

    except ImportError:
        pass


def _patch_httpx() -> None:
    """Patch httpx library for HTTP breadcrumbs."""
    try:
        import httpx

        if "httpx.Client.request" in _original_functions:
            return

        # Patch sync client
        original_sync = httpx.Client.request
        _original_functions["httpx.Client.request"] = original_sync

        def patched_sync_request(self, method, url, **kwargs):
            start = _now_ms()
            try:
                response = original_sync(self, method, url, **kwargs)
                duration = _now_ms() - start

                add_breadcrumb(
                    type="HTTP",
                    category="httpx",
                    message=f"{method} {url} [{response.status_code}]",
                    data={
                        "method": method,
                        "url": str(url)[:500],
                        "status_code": response.status_code,
                    },
                    duration_ms=duration,
                )

                return response
            except Exception as exc:
                duration = _now_ms() - start
                add_breadcrumb(
                    type="HTTP",
                    category="httpx",
                    message=f"{method} {url} [ERROR]",
                    data={"method": method, "url": str(url)[:500], "error": str(exc)[:200]},
                    duration_ms=duration,
                    level="error",
                )
                raise

        httpx.Client.request = patched_sync_request

        # Patch async client
        original_async = httpx.AsyncClient.request
        _original_functions["httpx.AsyncClient.request"] = original_async

        async def patched_async_request(self, method, url, **kwargs):
            start = _now_ms()
            try:
                response = await original_async(self, method, url, **kwargs)
                duration = _now_ms() - start

                add_breadcrumb(
                    type="HTTP",
                    category="httpx",
                    message=f"{method} {url} [{response.status_code}]",
                    data={
                        "method": method,
                        "url": str(url)[:500],
                        "status_code": response.status_code,
                    },
                    duration_ms=duration,
                )

                return response
            except Exception as exc:
                duration = _now_ms() - start
                add_breadcrumb(
                    type="HTTP",
                    category="httpx",
                    message=f"{method} {url} [ERROR]",
                    data={"method": method, "url": str(url)[:500], "error": str(exc)[:200]},
                    duration_ms=duration,
                    level="error",
                )
                raise

        httpx.AsyncClient.request = patched_async_request

    except ImportError:
        pass


def _patch_psycopg2() -> None:
    """Patch psycopg2 for SQL breadcrumbs."""
    try:
        import psycopg2
        from psycopg2.extensions import cursor as CursorClass

        if "psycopg2.cursor.execute" in _original_functions:
            return

        original_execute = CursorClass.execute
        _original_functions["psycopg2.cursor.execute"] = original_execute

        def patched_execute(self, query, vars=None):
            start = _now_ms()
            try:
                result = original_execute(self, query, vars)
                duration = _now_ms() - start

                # Truncate query for breadcrumb
                query_str = query if isinstance(query, str) else str(query)
                add_breadcrumb(
                    type="SQL",
                    category="psycopg2",
                    message=query_str[:100] + ("..." if len(query_str) > 100 else ""),
                    data={"query": query_str[:500]},
                    duration_ms=duration,
                )

                return result
            except Exception as exc:
                duration = _now_ms() - start
                query_str = query if isinstance(query, str) else str(query)
                add_breadcrumb(
                    type="SQL",
                    category="psycopg2",
                    message=f"SQL ERROR: {type(exc).__name__}",
                    data={"query": query_str[:500], "error": str(exc)[:200]},
                    duration_ms=duration,
                    level="error",
                )
                raise

        CursorClass.execute = patched_execute

    except ImportError:
        pass


def _patch_redis() -> None:
    """Patch redis-py for Redis breadcrumbs."""
    try:
        import redis

        if "redis.client.Redis.execute_command" in _original_functions:
            return

        original = redis.client.Redis.execute_command
        _original_functions["redis.client.Redis.execute_command"] = original

        def patched_execute_command(self, *args, **kwargs):
            start = _now_ms()
            command = args[0] if args else "UNKNOWN"
            try:
                result = original(self, *args, **kwargs)
                duration = _now_ms() - start

                # Build message
                cmd_args = " ".join(str(a)[:50] for a in args[:3])
                add_breadcrumb(
                    type="REDIS",
                    category="redis",
                    message=cmd_args[:100],
                    data={"command": command, "args": [str(a)[:100] for a in args[1:4]]},
                    duration_ms=duration,
                )

                return result
            except Exception as exc:
                duration = _now_ms() - start
                add_breadcrumb(
                    type="REDIS",
                    category="redis",
                    message=f"{command} ERROR: {type(exc).__name__}",
                    data={"command": command, "error": str(exc)[:200]},
                    duration_ms=duration,
                    level="error",
                )
                raise

        redis.client.Redis.execute_command = patched_execute_command

    except ImportError:
        pass


def _patch_logging() -> None:
    """Patch logging to capture log breadcrumbs."""
    import logging

    class BreadcrumbHandler(logging.Handler):
        """Logging handler that adds breadcrumbs."""

        def __init__(self, level=logging.WARNING):
            super().__init__(level)

        def emit(self, record: logging.LogRecord) -> None:
            try:
                level_map = {
                    logging.DEBUG: "debug",
                    logging.INFO: "info",
                    logging.WARNING: "warning",
                    logging.ERROR: "error",
                    logging.CRITICAL: "error",
                }

                add_breadcrumb(
                    type="LOG",
                    category=record.name,
                    message=record.getMessage()[:500],
                    data={
                        "level": record.levelname,
                        "logger": record.name,
                        "filename": record.filename,
                        "lineno": record.lineno,
                    },
                    level=level_map.get(record.levelno, "info"),
                )
            except Exception:
                pass  # Don't let breadcrumb capture break logging

    # Add handler to root logger
    root_logger = logging.getLogger()
    handler = BreadcrumbHandler(level=logging.WARNING)
    root_logger.addHandler(handler)


# =============================================================================
# Upload / Storage
# =============================================================================

def _upload_error(error_context: ErrorContext) -> None:
    """Upload error context to API or store locally."""
    if not _config:
        return

    payload = error_context.to_dict()

    # Add config info
    payload["project_id"] = _config.project_id
    payload["environment"] = _config.environment
    payload["release"] = _config.release

    if _config.debug:
        print(f"[hikaflow] Capturing error: {error_context.error_id}")
        print(f"[hikaflow] Exception: {error_context.exception_type}: {error_context.exception_message[:100]}")
        print(f"[hikaflow] Breadcrumbs: {len(error_context.request_context.breadcrumbs)}")

    # Store locally if configured
    if _config.local_storage_path:
        _store_locally(payload)

    # Upload to API
    if _config.api_key and HAS_REQUESTS:
        _upload_to_api(payload)


def _store_locally(payload: Dict[str, Any]) -> None:
    """Store error context locally."""
    if not _config or not _config.local_storage_path:
        return

    storage_dir = Path(_config.local_storage_path)
    storage_dir.mkdir(parents=True, exist_ok=True)

    error_id = payload.get("error_id")
    timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
    filename = f"{timestamp}_{error_id}.json.gz"

    filepath = storage_dir / filename

    # Compress and write
    json_bytes = json.dumps(payload, indent=2).encode("utf-8")
    with gzip.open(filepath, "wb") as f:
        f.write(json_bytes)

    if _config.debug:
        print(f"[hikaflow] Stored error locally: {filepath}")


def _upload_to_api(payload: Dict[str, Any]) -> None:
    """Upload error context to Hikaflow API."""
    if not _config or not _config.api_key or not HAS_REQUESTS:
        return

    try:
        # Compress payload
        json_bytes = json.dumps(payload).encode("utf-8")
        compressed = gzip.compress(json_bytes)

        response = requests_lib.post(
            f"{_config.api_endpoint.rstrip('/')}/v1/prod-errors",
            data=compressed,
            headers={
                "Authorization": f"Bearer {_config.api_key}",
                "Content-Type": "application/json",
                "Content-Encoding": "gzip",
                "X-Hikaflow-Project": _config.project_id or "",
            },
            timeout=5,
        )

        if _config.debug:
            print(f"[hikaflow] Upload response: {response.status_code}")

    except Exception as exc:
        if _config.debug:
            print(f"[hikaflow] Upload failed: {exc}")


# =============================================================================
# CLI for replaying production errors
# =============================================================================

def load_prod_error(error_path: str) -> Dict[str, Any]:
    """Load a production error from local storage.

    Args:
        error_path: Path to the error JSON file

    Returns:
        Error context dictionary
    """
    path = Path(error_path)

    if path.suffix == ".gz" or str(path).endswith(".json.gz"):
        with gzip.open(path, "rb") as f:
            return json.loads(f.read().decode("utf-8"))
    else:
        return json.loads(path.read_text())


def convert_to_replay_format(error_context: Dict[str, Any]) -> Dict[str, Any]:
    """Convert production error context to replay format.

    This transforms the production error breadcrumbs into the
    format expected by the test replay system.

    Args:
        error_context: Production error context

    Returns:
        Recording session format for replay
    """
    request = error_context.get("request", {})
    breadcrumbs = request.get("breadcrumbs", [])

    # Convert breadcrumbs to IO entries
    entries = []
    for bc in breadcrumbs:
        entry = {
            "seq": bc["seq"],
            "type": bc["type"],
            "timestamp": bc["timestamp"],
            "duration_ns": int(bc["duration_ms"] * 1_000_000),
        }

        # Convert based on type
        if bc["type"] == "HTTP":
            entry["request"] = {
                "method": bc["data"].get("method", "GET"),
                "url": bc["data"].get("url", ""),
            }
            entry["response"] = {
                "status_code": bc["data"].get("status_code", 0),
            }
        elif bc["type"] == "SQL":
            entry["request"] = {
                "query": bc["data"].get("query", ""),
            }
            entry["response"] = {"rows": [], "rows_affected": 0}
        elif bc["type"] == "REDIS":
            entry["request"] = {
                "command": bc["data"].get("command", ""),
                "args": bc["data"].get("args", []),
            }
            entry["response"] = {"value": None}

        entries.append(entry)

    # Build recording session
    session = {
        "id": error_context["error_id"],
        "start_time": request.get("started_at", error_context["timestamp"]),
        "end_time": error_context["timestamp"],
        "entries": entries,
        "environment": error_context.get("environment", {}),
        "outcome": "FAIL",
        "error_message": error_context["exception"]["message"],
        "error_stack": error_context["exception"]["stack"],
        "probe_version": "prod-capture",
        "runtime_version": error_context.get("runtime", {}).get("python_version", ""),
    }

    return session
