"""
Core SDK client.

``OvercastClient`` is the central object that owns the transport, scrubber,
global scope, and integration setup.  A module-level ``_client`` stores the
currently-active instance; integrations retrieve it via ``get_client()``.
"""

import os
import sys
import random
import traceback
from datetime import datetime, timezone

from overcast_sdk.consts import (
    SDK_VERSION, DEFAULT_OPTIONS, MAX_LOG_MESSAGE_LENGTH, MAX_TRACEBACK_LENGTH,
)
from overcast_sdk.utils import (
    capture_internal, debug_log, is_sending, safe_str, is_valid_sample_rate,
    truncate_traceback,
)
from overcast_sdk.serializer import safe_serialize
from overcast_sdk.scrubber import EventScrubber
from overcast_sdk.scope import (
    Scope, get_global_scope, set_global_scope,
    get_isolation_scope,
)
from overcast_sdk.transport import Transport

# ── Module-level client reference ─────────────────────────────────────────────
_client = None


def get_client():
    """Return the active ``OvercastClient``, or ``None``."""
    return _client


def set_client(client):
    global _client
    _client = client


# ═══════════════════════════════════════════════════════════════════════════════
# OvercastClient
# ═══════════════════════════════════════════════════════════════════════════════

class OvercastClient:
    """Owns transport, scrubber, scope, and integration lifecycle."""

    def __init__(self, options):
        self.options = self._validate_options(options)
        self.transport = Transport(self.options)
        self.scrubber = EventScrubber(
            send_default_pii=self.options.get("send_default_pii", False),
        )
        # Global scope
        gs = get_global_scope()
        gs.set_context("sdk", {
            "name": "overcast-python",
            "version": SDK_VERSION,
        })
        gs.set_context("runtime", {
            "name": "python",
            "version": sys.version.split()[0],
        })

    # ── Option validation ─────────────────────────────────────────────────

    @staticmethod
    def _validate_options(raw):
        opts = dict(DEFAULT_OPTIONS)
        opts.update(raw)

        # --- api_key ---
        api_key = opts.get("api_key", "")
        if not api_key or not isinstance(api_key, str):
            raise ValueError(
                "api_key is required.  Get yours at https://overcastsre.com/settings"
            )

        # --- base_url ---
        base_url = opts.get("base_url", "")
        if not base_url.startswith(("http://", "https://")):
            raise ValueError(f"Invalid base_url: {base_url}")

        # --- sample rate ---
        sr = opts.get("log_sampling_rate", 1.0)
        if not is_valid_sample_rate(sr):
            debug_log(f"Invalid log_sampling_rate={sr}, defaulting to 1.0")
            opts["log_sampling_rate"] = 1.0

        # --- batch_size ---
        bs = opts.get("batch_size", 50)
        if not isinstance(bs, int) or bs < 1:
            debug_log(f"Invalid batch_size={bs}, defaulting to 50")
            opts["batch_size"] = 50

        # --- flush_interval ---
        fi = opts.get("flush_interval", 5.0)
        if not isinstance(fi, (int, float)) or fi <= 0:
            debug_log(f"Invalid flush_interval={fi}, defaulting to 5.0")
            opts["flush_interval"] = 5.0

        # --- hooks ---
        for hook_name in ("before_send", "before_breadcrumb"):
            hook = opts.get(hook_name)
            if hook is not None and not callable(hook):
                raise ValueError(f"{hook_name} must be callable")

        return opts

    # ── Is active? ────────────────────────────────────────────────────────

    def is_active(self):
        return True

    # ── Core: enqueue a log event ─────────────────────────────────────────

    def enqueue(self, level, message, metadata=None):
        """Build an event dict, apply scope + scrubbing, and submit."""
        if is_sending():
            return

        with capture_internal:
            self._enqueue_impl(level, message, metadata)

    def _enqueue_impl(self, level, message, metadata):
        if metadata is None:
            metadata = {}

        # ── Sampling (skip non-error events if sampled out) ───────────
        if level not in ("ERROR", "WARNING"):
            base_rate = self.options.get("log_sampling_rate", 1.0)
            effective = self.transport.monitor.effective_sample_rate(base_rate)
            if effective < 1.0 and random.random() > effective:
                self.transport.record_lost_event("sample_rate")
                return

        # ── Build event ───────────────────────────────────────────────
        event = {
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "level": level,
            "message": safe_str(message, MAX_LOG_MESSAGE_LENGTH),
            "service": self.options["service_name"],
            "environment": self.options["environment"],
            "raw_log": safe_str(message, MAX_LOG_MESSAGE_LENGTH),
            "metadata": {
                **safe_serialize(metadata),
                "sdk_language": "python",
                "sdk_version": SDK_VERSION,
                "python_version": sys.version.split()[0],
                "pid": os.getpid(),
            },
        }

        # Append stack trace to message if present
        tb = metadata.get("error", {}).get("traceback") if isinstance(metadata.get("error"), dict) else None
        if tb:
            tb = truncate_traceback(tb, MAX_TRACEBACK_LENGTH)
            event["message"] += f"\n\nStack trace:\n{tb}"
            event["raw_log"] = event["message"]

        # ── Apply scopes ──────────────────────────────────────────────
        global_scope = get_global_scope()
        isolation_scope = get_isolation_scope()

        event = global_scope.apply_to_event(event)
        if event is None:
            return
        event = isolation_scope.apply_to_event(event)
        if event is None:
            return

        # ── Scrub PII / sensitive data ────────────────────────────────
        if self.options.get("sanitize", True):
            event = self.scrubber.scrub_event(event)

        # ── Strip PII fields unless opted-in ──────────────────────────
        if not self.options.get("send_default_pii", False):
            md = event.get("metadata", {})
            for pii_key in ("ip", "user_agent", "remote_addr"):
                md.pop(pii_key, None)

        # ── before_send hook ──────────────────────────────────────────
        before_send = self.options.get("before_send")
        if before_send is not None:
            try:
                event = before_send(event)
            except Exception:
                with capture_internal:
                    raise
            if event is None:
                return  # user chose to drop this event

        # ── Submit to transport ───────────────────────────────────────
        self.transport.enqueue(event)

    # ── Manual exception capture ──────────────────────────────────────────

    def capture_exception(self, exc, **metadata):
        if not isinstance(exc, BaseException):
            exc = Exception(str(exc))
        tb = "".join(
            traceback.format_exception(type(exc), exc, exc.__traceback__)
        )
        self.enqueue("ERROR", str(exc), {
            **metadata,
            "type": "captured_exception",
            "error": {
                "type": type(exc).__name__,
                "message": str(exc),
                "traceback": tb,
            },
        })

    # ── Flush / close ─────────────────────────────────────────────────────

    def flush(self, timeout=5.0):
        self.transport.flush(timeout)

    def close(self):
        self.transport.close()
