"""Server-side crash reporting for the Python analytics SDK.

Provides ``capture_exception()`` + automatic global ``sys.excepthook``
and ``threading.excepthook`` capture.  Crash reports are sent to
``POST /api/crash-reports`` using the same wire format as the client SDKs.
"""

from __future__ import annotations

import datetime
import logging
import os
import platform
import sys
import threading
import traceback
import uuid
from typing import Any, Dict, List, Literal, Optional

import httpx

from .types import ServerContext

logger = logging.getLogger("kanban_analytics.crash")

# ---------------------------------------------------------------------------
# Types
# ---------------------------------------------------------------------------

CrashSeverity = Literal["fatal", "error", "warning"]


class BreadcrumbDict:
    """Typed breadcrumb dict shape."""

    type: str
    category: Optional[str]
    message: Optional[str]
    data: Optional[Dict[str, Any]]
    timestamp: str
    level: Optional[CrashSeverity]


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

DEFAULT_CRASH_ENDPOINT = "/api/crash-reports"
DEFAULT_MAX_BREADCRUMBS = 100
MAX_CRASH_BUFFER = 20
DEFAULT_TIMEOUT = 5.0
MAX_RETRIES = 3

# ---------------------------------------------------------------------------
# CrashReporter
# ---------------------------------------------------------------------------


class CrashReporter:
    """Synchronous crash reporter for server-side Python.

    Initialise with the same ``api_key`` and ``service_name`` as the
    main analytics client.

    Example::

        from kanban_analytics.crash import CrashReporter

        reporter = CrashReporter(
            api_key="pk_...",
            crash_endpoint="https://your-app.com/api/crash-reports",
            service_name="my-service",
        )
        reporter.init(server_context)

        try:
            do_something()
        except Exception as e:
            reporter.capture_exception(e)
    """

    def __init__(
        self,
        *,
        api_key: str,
        crash_endpoint: str | None = None,
        max_breadcrumbs: int = DEFAULT_MAX_BREADCRUMBS,
        auto_capture: bool = True,
        debug: bool = False,
        timeout: float = DEFAULT_TIMEOUT,
        service_name: str | None = None,
    ) -> None:
        self._api_key = api_key
        self._crash_endpoint = crash_endpoint or DEFAULT_CRASH_ENDPOINT
        self._max_breadcrumbs = max_breadcrumbs
        self._auto_capture = auto_capture
        self._debug = debug
        self._timeout = timeout
        self._service_name = service_name or "unknown-service"

        self._server_context: ServerContext | None = None
        self._breadcrumbs: List[Dict[str, Any]] = []
        self._crash_buffer: List[Dict[str, Any]] = []
        self._client: httpx.Client | None = None
        self._initialized = False
        self._lock = threading.Lock()

        # Original hooks for cleanup
        self._original_excepthook: Any = None
        self._original_threading_excepthook: Any = None

    # -- Lifecycle ----------------------------------------------------------

    def init(self, server_context: ServerContext) -> None:
        """Initialise with server context from the main SDK."""
        self._server_context = server_context
        self._client = httpx.Client(timeout=self._timeout)
        self._initialized = True

        if self._auto_capture:
            self._install_global_handlers()

        self._debug_log("CrashReporter initialized")

    def destroy(self) -> None:
        """Flush remaining reports and clean up."""
        self._remove_global_handlers()
        self.flush()
        self._breadcrumbs.clear()
        self._crash_buffer.clear()
        if self._client:
            self._client.close()
            self._client = None
        self._initialized = False
        self._debug_log("CrashReporter destroyed")

    # -- Public API ---------------------------------------------------------

    def capture_exception(
        self,
        error: BaseException | str,
        *,
        severity: CrashSeverity = "error",
        handled: bool = True,
        tags: Dict[str, str] | None = None,
        extra: Dict[str, Any] | None = None,
        user_id: str | None = None,
    ) -> None:
        """Capture an exception and enqueue it as a crash report."""
        if not self._initialized:
            self._debug_log("capture_exception called before init, ignoring")
            return

        if isinstance(error, str):
            error_message = error
            error_type = "Error"
            stack_trace = None
        else:
            error_message = str(error) or type(error).__name__
            error_type = type(error).__qualname__
            stack_trace = "".join(traceback.format_exception(type(error), error, error.__traceback__))

        report: Dict[str, Any] = {
            "messageId": str(uuid.uuid4()),
            "anonymousId": self._service_name,
            "userId": user_id,
            "sessionId": f"server-{self._service_name}",
            "timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
            "errorMessage": error_message,
            "errorType": error_type,
            "stackTrace": stack_trace,
            "severity": severity,
            "handled": handled,
            "context": {
                **(self._server_context or {}),
                **(extra or {}),
            },
            "breadcrumbs": list(self._breadcrumbs),
            "appState": self._collect_app_state(),
            "tags": tags,
        }

        self._enqueue_report(report)

    def add_breadcrumb(
        self,
        *,
        type: str = "custom",
        category: str | None = None,
        message: str | None = None,
        data: Dict[str, Any] | None = None,
        level: CrashSeverity | None = None,
    ) -> None:
        """Add a breadcrumb for context-building in crash reports."""
        crumb: Dict[str, Any] = {
            "type": type,
            "timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
        }
        if category is not None:
            crumb["category"] = category
        if message is not None:
            crumb["message"] = message
        if data is not None:
            crumb["data"] = data
        if level is not None:
            crumb["level"] = level

        self._breadcrumbs.append(crumb)
        if len(self._breadcrumbs) > self._max_breadcrumbs:
            self._breadcrumbs = self._breadcrumbs[-self._max_breadcrumbs :]

    def flush(self) -> None:
        """Flush any buffered crash reports immediately."""
        with self._lock:
            if not self._crash_buffer:
                return
            batch = list(self._crash_buffer)
            self._crash_buffer.clear()

        self._send_batch_with_retry(batch)

    # -- Private ------------------------------------------------------------

    def _enqueue_report(self, report: Dict[str, Any]) -> None:
        batch: List[Dict[str, Any]] | None = None
        with self._lock:
            self._crash_buffer.append(report)
            self._debug_log(
                f"crash report enqueued: {report.get('errorType')}: "
                f"{report.get('errorMessage')}"
            )

            if len(self._crash_buffer) >= MAX_CRASH_BUFFER:
                batch = list(self._crash_buffer)
                self._crash_buffer.clear()

        if batch is None:
            return

        # Flush outside the lock
        self._send_batch_with_retry(batch)

    def _send_batch_with_retry(self, batch: List[Dict[str, Any]]) -> None:
        for attempt in range(MAX_RETRIES):
            try:
                if self._client is None:
                    return

                response = self._client.post(
                    self._crash_endpoint,
                    json={
                        "batch": batch,
                        "sentAt": datetime.datetime.now(
                            datetime.timezone.utc
                        ).isoformat(),
                    },
                    headers={
                        "Content-Type": "application/json",
                        "X-API-Key": self._api_key,
                    },
                    timeout=self._timeout,
                )

                if 200 <= response.status_code < 300:
                    self._debug_log(f"flushed {len(batch)} crash report(s)")
                    return

                if 400 <= response.status_code < 500:
                    self._debug_log(
                        f"crash report rejected ({response.status_code}), dropping"
                    )
                    return

            except Exception as e:
                self._debug_log(f"crash report send failed: {e}")

            # Exponential backoff
            if attempt < MAX_RETRIES - 1:
                import time

                time.sleep(1.0 * (2**attempt))

        self._debug_log(
            f"crash report send failed after {MAX_RETRIES} retries, dropping"
        )

    def _collect_app_state(self) -> Dict[str, Any]:
        state: Dict[str, Any] = {
            "python_version": platform.python_version(),
            "platform": sys.platform,
            "arch": platform.machine(),
            "pid": os.getpid(),
        }

        try:
            import resource

            usage = resource.getrusage(resource.RUSAGE_SELF)
            state["memory_max_rss_kb"] = usage.ru_maxrss
        except ImportError:
            pass

        return state

    # -- Global Error Handlers ----------------------------------------------

    def _install_global_handlers(self) -> None:
        """Override sys.excepthook and threading.excepthook."""
        self._original_excepthook = sys.excepthook

        def _excepthook(
            exc_type: type[BaseException],
            exc_value: BaseException,
            exc_tb: Any,
        ) -> None:
            self.capture_exception(
                exc_value,
                severity="fatal",
                handled=False,
                tags={"source": "sys.excepthook"},
            )
            self.flush()  # Sync flush before process exits
            if self._original_excepthook:
                self._original_excepthook(exc_type, exc_value, exc_tb)

        sys.excepthook = _excepthook

        # threading.excepthook (Python 3.8+)
        if hasattr(threading, "excepthook"):
            self._original_threading_excepthook = threading.excepthook

            def _threading_excepthook(args: Any) -> None:
                if args.exc_value is not None:
                    self.capture_exception(
                        args.exc_value,
                        severity="error",
                        handled=False,
                        tags={"source": "threading.excepthook"},
                    )
                if self._original_threading_excepthook:
                    self._original_threading_excepthook(args)

            threading.excepthook = _threading_excepthook

    def _remove_global_handlers(self) -> None:
        """Restore original exception hooks."""
        if self._original_excepthook is not None:
            sys.excepthook = self._original_excepthook
            self._original_excepthook = None

        if self._original_threading_excepthook is not None and hasattr(
            threading, "excepthook"
        ):
            threading.excepthook = self._original_threading_excepthook
            self._original_threading_excepthook = None

    def _debug_log(self, message: str) -> None:
        if self._debug:
            logger.debug("[CrashReporter] %s", message)
