"""Background worker that drains the event buffer and sends to the Genosis API.

Single persistent connection. Batched writes. Acknowledged receipts.
Runs in a daemon thread — dies when the main process exits.
"""

from __future__ import annotations

import atexit
import logging
import signal
import threading
import time
from typing import Any, Optional

from .buffer import DiskBuffer

logger = logging.getLogger("genosis")

# How often the worker checks the buffer (seconds)
DEFAULT_DRAIN_INTERVAL = 1.0

# Max events per batch POST
DEFAULT_BATCH_SIZE = 500

# How long to wait before retrying after a failure
RETRY_BACKOFF_BASE = 2.0
MAX_RETRY_BACKOFF = 60.0


class BackgroundWorker:
    """Drains the DiskBuffer and sends events to the Genosis API.

    Uses a persistent httpx.Client for connection pooling.
    All communication with the Genosis API happens here — never
    on the main thread.
    """

    def __init__(
        self,
        buffer: DiskBuffer,
        base_url: str,
        api_key: str,
        timeout: float = 30.0,
        drain_interval: float = DEFAULT_DRAIN_INTERVAL,
        batch_size: int = DEFAULT_BATCH_SIZE,
    ) -> None:
        self._buffer = buffer
        self._base_url = base_url.rstrip("/")
        self._api_key = api_key
        self._timeout = timeout
        self._drain_interval = drain_interval
        self._batch_size = batch_size

        self._stop_event = threading.Event()
        self._thread: Optional[threading.Thread] = None
        self._consecutive_failures = 0

        # Lazy import to avoid circular deps
        import httpx
        self._client = httpx.Client(
            base_url=self._base_url,
            headers={
                "Authorization": f"Bearer {self._api_key}",
                "Content-Type": "application/json",
                "User-Agent": "genosis-sdk-python/1.0.0",
            },
            timeout=self._timeout,
        )

        # Register shutdown handlers — ensure buffer drains on exit
        atexit.register(self.shutdown)
        # Signal handlers for containerized environments (SIGTERM from Docker/K8s)
        try:
            signal.signal(signal.SIGTERM, lambda *_: self.shutdown())
        except (OSError, ValueError):
            pass  # Can't set signal handler from non-main thread

    def start(self) -> None:
        """Start the background worker thread."""
        if self._thread and self._thread.is_alive():
            return
        self._stop_event.clear()
        self._thread = threading.Thread(target=self._run, daemon=True, name="genosis-worker")
        self._thread.start()

    def shutdown(self, timeout: float = 60.0) -> None:
        """Stop the worker and flush ALL remaining events.

        Blocks until the buffer is fully drained or timeout expires.
        Default 60 seconds — generous because unflushed telemetry means
        lost revenue (savings not tracked = customer not billed).

        For serverless/Lambda, call g.flush() explicitly before returning.
        """
        self._stop_event.set()
        if self._thread and self._thread.is_alive():
            self._thread.join(timeout=min(timeout, 5.0))

        # Aggressively drain remaining events
        start = time.time()
        flushed = 0
        while self._buffer.size() > 0 and (time.time() - start) < timeout:
            try:
                if self._drain_once():
                    flushed += 1
                else:
                    break  # Nothing left to drain
            except Exception:
                time.sleep(0.5)  # Brief pause on error, then retry

        remaining = self._buffer.size()
        if remaining > 0:
            logger.warning(
                "genosis worker: %d events still in buffer after %.1fs shutdown (of %d flushed)",
                remaining, time.time() - start, flushed,
            )

        try:
            self._client.close()
        except Exception:
            pass

    def _run(self) -> None:
        """Main worker loop. Runs until stop_event is set."""
        while not self._stop_event.is_set():
            try:
                drained = self._drain_once()
                if drained:
                    self._consecutive_failures = 0
                else:
                    # Nothing to drain — sleep for the interval
                    self._stop_event.wait(self._drain_interval)
            except Exception as exc:
                self._consecutive_failures += 1
                backoff = min(
                    RETRY_BACKOFF_BASE ** self._consecutive_failures,
                    MAX_RETRY_BACKOFF,
                )
                logger.debug("genosis worker: error (%s), backing off %.1fs", exc, backoff)
                self._stop_event.wait(backoff)

    def _drain_once(self) -> bool:
        """Drain one batch from the buffer. Returns True if events were sent."""
        batch = self._buffer.peek(self._batch_size)
        if not batch:
            return False

        # Group by type
        telemetry_events = [e for e in batch if e["type"] == "telemetry"]
        other_events = [e for e in batch if e["type"] != "telemetry"]

        confirmed_ids: list[str] = []

        # Send telemetry as a batch
        if telemetry_events:
            acked = self._send_telemetry_batch(telemetry_events)
            confirmed_ids.extend(acked)

        # Send other events individually (manifest acks, error reports, etc.)
        for event in other_events:
            if self._send_single(event):
                confirmed_ids.append(event["id"])

        # Remove confirmed events from buffer
        if confirmed_ids:
            self._buffer.remove(confirmed_ids)

        return len(confirmed_ids) > 0

    def _send_telemetry_batch(self, events: list[dict[str, Any]]) -> list[str]:
        """Send a batch of telemetry events. Returns list of acknowledged IDs."""
        payloads = [e["payload"] for e in events]
        try:
            resp = self._client.post(
                "/v1/ingest",
                json={"events": payloads},
            )
            if resp.is_success:
                # Server acknowledged the batch — all events confirmed
                return [e["id"] for e in events]
            else:
                logger.debug(
                    "genosis worker: telemetry batch failed (%d): %s",
                    resp.status_code,
                    resp.text[:200],
                )
                return []
        except Exception as exc:
            logger.debug("genosis worker: telemetry send error: %s", exc)
            return []

    def _send_single(self, event: dict[str, Any]) -> bool:
        """Send a single non-telemetry event. Returns True on success."""
        event_type = event["type"]
        payload = event["payload"]

        try:
            if event_type == "manifest_ack":
                resp = self._client.post("/v1/manifest/ack", json=payload)
            elif event_type == "error_report":
                resp = self._client.post("/v1/errors", json=payload)
            elif event_type == "optimization_trigger":
                resp = self._client.post("/v1/optimize", json=payload)
            else:
                logger.debug("genosis worker: unknown event type: %s", event_type)
                return True  # Discard unknown types

            return resp.is_success
        except Exception as exc:
            logger.debug("genosis worker: send error (%s): %s", event_type, exc)
            return False
