"""
Transport layer — background worker, batching, compression, rate-limiting.

All network I/O happens inside a single persistent daemon thread managed
by ``BackgroundWorker``.  The ``Transport`` class owns the worker, a
bounded buffer, periodic flush timer, and all retry / rate-limit logic.
"""

import gzip
import json
import os
import sys
import time
import threading
from collections import deque
from datetime import datetime, timedelta, timezone
from queue import Queue, Full
from urllib.request import Request, urlopen

from overcast_sdk.consts import (
    SDK_VERSION, SDK_NAME,
    MAX_BUFFER_SIZE, MAX_QUEUE_SIZE, MAX_REQUEUE_ON_FAILURE,
    DEFAULT_BATCH_SIZE, DEFAULT_FLUSH_INTERVAL, DEFAULT_SEND_TIMEOUT,
    INGEST_ENDPOINT, HEALTH_CHECK_INTERVAL, MAX_DOWNSAMPLE_FACTOR,
)
from overcast_sdk.utils import (
    capture_internal, debug_log, set_sending, encrypt_payload,
)

_SHUTDOWN_SENTINEL = object()


# ═══════════════════════════════════════════════════════════════════════════════
# Background Worker
# ═══════════════════════════════════════════════════════════════════════════════

class BackgroundWorker:
    """Persistent daemon thread that drains a bounded queue."""

    def __init__(self, queue_size=MAX_QUEUE_SIZE):
        self._queue = Queue(maxsize=queue_size)
        self._lock = threading.Lock()
        self._thread = None
        self._thread_pid = None
        self._flush_event = threading.Event()

    # ── Public interface ──────────────────────────────────────────────────

    def submit(self, callback):
        """Enqueue *callback* for execution.  Returns False if full."""
        self._ensure_thread()
        try:
            self._queue.put_nowait(callback)
            return True
        except Full:
            return False

    def flush(self, timeout=5.0):
        """Block until all pending work completes (up to *timeout* s)."""
        if not self.is_alive:
            return
        self._flush_event.clear()
        self._queue.put_nowait(lambda: self._flush_event.set())
        self._flush_event.wait(timeout=timeout)

    def kill(self):
        """Signal the worker to exit and wait for it to finish."""
        with self._lock:
            if self._thread is not None:
                # Drain the queue so we can always enqueue the sentinel
                while not self._queue.empty():
                    try:
                        self._queue.get_nowait()
                        self._queue.task_done()
                    except Exception:
                        break
                try:
                    self._queue.put_nowait(_SHUTDOWN_SENTINEL)
                except Full:
                    pass
                thread = self._thread
                self._thread = None
                self._thread_pid = None
            else:
                return
        # Wait outside the lock so the thread can finish
        if thread is not None:
            thread.join(timeout=2.0)

    @property
    def is_alive(self):
        if self._thread_pid != os.getpid():
            return False
        return self._thread is not None and self._thread.is_alive()

    def full(self):
        return self._queue.full()

    # ── Internals ─────────────────────────────────────────────────────────

    def _ensure_thread(self):
        if self.is_alive:
            return
        with self._lock:
            if self.is_alive:
                return
            t = threading.Thread(
                target=self._run,
                name="overcast-sdk.worker",
                daemon=True,
            )
            try:
                t.start()
                self._thread = t
                self._thread_pid = os.getpid()
            except RuntimeError:
                self._thread = None

    def _run(self):
        while True:
            callback = self._queue.get()
            try:
                if callback is _SHUTDOWN_SENTINEL:
                    break
                callback()
            except Exception:
                with capture_internal:
                    raise
            finally:
                self._queue.task_done()
            time.sleep(0)


# ═══════════════════════════════════════════════════════════════════════════════
# Health Monitor
# ═══════════════════════════════════════════════════════════════════════════════

class HealthMonitor:
    """Periodically checks transport health and adjusts sampling."""

    def __init__(self, transport, interval=HEALTH_CHECK_INTERVAL):
        self._transport = transport
        self._interval = interval
        self._downsample_factor = 0
        self._thread = None
        self._running = True

    def start(self):
        self._thread = threading.Thread(
            target=self._run,
            name="overcast-sdk.monitor",
            daemon=True,
        )
        self._thread.start()

    def stop(self):
        self._running = False

    def effective_sample_rate(self, base_rate):
        """Return the base rate reduced by the current downsample factor."""
        return base_rate * (0.5 ** self._downsample_factor)

    @property
    def downsample_factor(self):
        return self._downsample_factor

    def _run(self):
        while self._running:
            time.sleep(self._interval)
            if self._transport.is_healthy():
                self._downsample_factor = max(0, self._downsample_factor - 1)
            else:
                self._downsample_factor = min(
                    self._downsample_factor + 1, MAX_DOWNSAMPLE_FACTOR,
                )
                debug_log(
                    f"[Monitor] unhealthy, downsample factor={self._downsample_factor}"
                )


# ═══════════════════════════════════════════════════════════════════════════════
# Transport
# ═══════════════════════════════════════════════════════════════════════════════

class Transport:
    """Manages log buffering, batching, and HTTP delivery."""

    def __init__(self, options):
        self._options = options
        self._base_url = options.get("base_url", "")
        self._api_key = options.get("api_key", "")
        self._service_name = options.get("service_name", "python-app")
        self._batch_size = options.get("batch_size", DEFAULT_BATCH_SIZE)
        self._flush_interval = options.get("flush_interval", DEFAULT_FLUSH_INTERVAL)
        self._timeout = DEFAULT_SEND_TIMEOUT
        self._compress = options.get("enable_compression", True)
        self._closed = False

        # Buffer
        max_buf = options.get("max_buffer_size", MAX_BUFFER_SIZE)
        self._buffer = deque(maxlen=max_buf)
        self._buffer_lock = threading.Lock()

        # Worker
        queue_size = options.get("queue_size", MAX_QUEUE_SIZE)
        self._worker = BackgroundWorker(queue_size)

        # Rate limiting
        self._disabled_until = {}          # category -> datetime

        # Client reports (lost event tracking)
        self._discarded_events = {}        # (category, reason) -> int
        self._discarded_lock = threading.Lock()

        # Flush timer
        self._flush_timer = None
        self._start_flush_timer()

        # Health monitor
        self._monitor = HealthMonitor(self)
        self._monitor.start()

    # ── Buffer / enqueue ──────────────────────────────────────────────────

    def enqueue(self, entry):
        """Add a log entry dict to the buffer."""
        should_flush = False
        with self._buffer_lock:
            self._buffer.append(entry)
            should_flush = (
                entry.get("level") == "ERROR"
                or len(self._buffer) >= self._batch_size
            )

        if should_flush:
            self.flush_async()

    def flush_async(self):
        """Drain the buffer and submit a send job to the worker."""
        with self._buffer_lock:
            if not self._buffer:
                return
            logs = list(self._buffer)
            self._buffer.clear()

        if self._check_rate_limited():
            self.record_lost_event("ratelimit_backoff", len(logs))
            return

        if not self._worker.submit(lambda: self._send(logs)):
            self.record_lost_event("queue_overflow", len(logs))

    def flush(self, timeout=5.0):
        """Synchronous flush: drain buffer then wait for worker to finish."""
        self.flush_async()
        self._worker.flush(timeout)

    def close(self):
        """Shut down the transport cleanly."""
        self._closed = True
        if self._flush_timer:
            self._flush_timer.cancel()
            self._flush_timer = None
        self._monitor.stop()
        self.flush(timeout=5.0)
        self._worker.kill()

    # ── Health ────────────────────────────────────────────────────────────

    def is_healthy(self):
        return not self._worker.full() and not self._check_rate_limited()

    @property
    def monitor(self):
        return self._monitor

    # ── Rate limiting ─────────────────────────────────────────────────────

    def _check_rate_limited(self):
        now = datetime.now(timezone.utc)
        return any(ts > now for ts in self._disabled_until.values())

    def _update_rate_limits(self, status_code, headers):
        if status_code == 429:
            try:
                retry_after = int(headers.get("Retry-After", 60))
            except (ValueError, TypeError):
                retry_after = 60
            self._disabled_until[None] = (
                datetime.now(timezone.utc) + timedelta(seconds=retry_after)
            )
            debug_log(f"Rate-limited for {retry_after}s")

    # ── Lost-event tracking ───────────────────────────────────────────────

    def record_lost_event(self, reason, quantity=1):
        with self._discarded_lock:
            key = ("log", reason)
            self._discarded_events[key] = (
                self._discarded_events.get(key, 0) + quantity
            )

    def get_lost_event_summary(self):
        with self._discarded_lock:
            summary = dict(self._discarded_events)
            self._discarded_events.clear()
        return summary

    # ── Sending ───────────────────────────────────────────────────────────

    def _send(self, logs):
        set_sending(True)
        try:
            py_version = sys.version.split()[0]
            body_dict = {
                "api_key": self._api_key,
                "source_type": "application",
                "source_description": f"{self._service_name} (python/{py_version})",
                "logs": logs,
            }
            body_str = json.dumps(body_dict, default=str)

            # Encryption
            body_str = encrypt_payload(body_str, self._options)

            body_bytes = body_str.encode("utf-8")

            # Compression
            content_encoding = None
            if self._compress:
                compressed = gzip.compress(body_bytes)
                if len(compressed) < len(body_bytes):
                    body_bytes = compressed
                    content_encoding = "gzip"

            url = f"{self._base_url}{INGEST_ENDPOINT}"
            headers = {
                "Content-Type": "application/json",
                "X-Overcast-Key": self._api_key,
                "X-Overcast-Service": self._service_name,
                "X-Overcast-SDK": f"python/{SDK_VERSION}",
                "User-Agent": f"{SDK_NAME}/{SDK_VERSION} python/{py_version}",
            }
            if content_encoding:
                headers["Content-Encoding"] = content_encoding

            req = Request(url, data=body_bytes, headers=headers, method="POST")
            resp = urlopen(req, timeout=self._timeout)
            status = resp.status

            # Read response headers for rate limits
            resp_headers = {k: v for k, v in resp.getheaders()}
            self._update_rate_limits(status, resp_headers)

        except Exception as e:
            debug_log(f"Failed to send logs: {e}")

            # Check if it was a 429
            status_code = getattr(e, "code", None) or getattr(e, "status", None)
            if status_code == 429:
                resp_headers = {}
                if hasattr(e, "headers"):
                    resp_headers = dict(e.headers) if e.headers else {}
                self._update_rate_limits(429, resp_headers)
                self.record_lost_event("ratelimit_backoff", len(logs))
            elif status_code == 413:
                self.record_lost_event("payload_too_large", len(logs))
            else:
                # Best-effort re-enqueue
                with self._buffer_lock:
                    for entry in logs[:MAX_REQUEUE_ON_FAILURE]:
                        self._buffer.appendleft(entry)
        finally:
            set_sending(False)

    # ── Flush timer ───────────────────────────────────────────────────────

    def _start_flush_timer(self):
        if self._closed:
            return

        def _tick():
            if self._closed:
                return
            with capture_internal:
                self.flush_async()
            self._start_flush_timer()

        self._flush_timer = threading.Timer(self._flush_interval, _tick)
        self._flush_timer.daemon = True
        self._flush_timer.start()
