# Copyright (c) Metis. All rights reserved.

"""Background exposure logging for A/B experiments.

This module provides an ExposureLogger that batches and sends exposure
records to the backend asynchronously. This follows the same pattern as
the heartbeat thread in run.py - a daemon thread that flushes periodically.

Key features:
- Batches exposures to reduce API calls (default: 100 per batch)
- Flushes on interval (default: 1 second) even if batch isn't full
- Re-queues failed sends with a limit to avoid memory issues
- Graceful shutdown with final flush on stop()
"""

from __future__ import annotations

import base64
import logging
import queue
import threading
import time
from typing import List, Optional

import requests

from .types import Exposure

logger = logging.getLogger(__name__)

# Maximum number of exposures to re-queue on failure
MAX_REQUEUE_SIZE = 500


class ExposureLogger:
    """Background thread that batches and flushes exposures to the backend.

    Usage::

        logger = ExposureLogger(
            api_endpoint="https://insight.example.com/api/public/experiments/exposures/batch",
            public_key="pk-lf-xxx",
            secret_key="sk-lf-xxx",
        )
        logger.start()

        # Log exposures (non-blocking)
        logger.log(Exposure(
            user_id="user-123",
            experiment_id="prompt-test-v1",
            variant_key="control",
            timestamp="2024-01-15T10:30:00Z",
        ))

        # On shutdown
        logger.stop()  # Flushes remaining exposures
    """

    def __init__(
        self,
        api_endpoint: str,
        public_key: str,
        secret_key: str,
        batch_size: int = 100,
        flush_interval: float = 1.0,
        request_timeout: float = 10.0,
    ):
        """Initialize the exposure logger.

        Args:
            api_endpoint: Full URL for the batch exposures API
            public_key: Insight public key (pk-lf-...)
            secret_key: Insight secret key (sk-lf-...)
            batch_size: Number of exposures to batch before sending
            flush_interval: Seconds between flushes (even if batch not full)
            request_timeout: Timeout for API requests in seconds
        """
        self._endpoint = api_endpoint
        self._public_key = public_key
        self._secret_key = secret_key
        self._batch_size = batch_size
        self._flush_interval = flush_interval
        self._request_timeout = request_timeout

        # Thread-safe queue for incoming exposures
        self._queue: queue.Queue[Exposure] = queue.Queue()

        # Buffer for batching — only accessed by the background thread in _run()
        self._buffer: List[Exposure] = []

        # Thread management
        self._thread: Optional[threading.Thread] = None
        self._shutdown = threading.Event()
        self._started = False

    def _basic_auth_header(self) -> str:
        """Build Basic Auth header from public/secret keys."""
        credentials = f"{self._public_key}:{self._secret_key}"
        encoded = base64.b64encode(credentials.encode("utf-8")).decode("utf-8")
        return f"Basic {encoded}"

    def start(self) -> None:
        """Start the background flusher thread.

        Safe to call multiple times - only starts once.
        """
        if self._started:
            return

        self._shutdown.clear()
        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()
        self._started = True
        logger.debug("ExposureLogger started")

    def stop(self) -> None:
        """Stop the background thread and flush remaining exposures.

        Blocks until the thread finishes (up to 5 seconds).
        """
        if not self._started:
            return

        self._shutdown.set()
        if self._thread:
            self._thread.join(timeout=5.0)

        self._started = False
        logger.debug("ExposureLogger stopped")

    def log(self, exposure: Exposure) -> None:
        """Queue an exposure for batched sending.

        This is non-blocking - the exposure is added to a queue and
        sent by the background thread.

        Args:
            exposure: The exposure record to log
        """
        if not self._started:
            logger.warning("ExposureLogger not started, exposure will be dropped")
            return

        self._queue.put(exposure)

    def _run(self) -> None:
        """Background loop: drain queue, flush on interval or batch size."""
        last_flush = time.monotonic()

        while not self._shutdown.is_set():
            try:
                # Try to get an exposure with a short timeout
                exposure = self._queue.get(timeout=0.1)
                self._buffer.append(exposure)

                # Flush if batch is full
                if len(self._buffer) >= self._batch_size:
                    self._flush()
                    last_flush = time.monotonic()
            except queue.Empty:
                # No new exposures, check if we should flush on interval
                if time.monotonic() - last_flush >= self._flush_interval:
                    self._flush()
                    last_flush = time.monotonic()

        # Final flush on shutdown: drain remaining queue items
        while True:
            try:
                self._buffer.append(self._queue.get_nowait())
            except queue.Empty:
                break

        self._flush()

    def _flush(self) -> None:
        """Send buffered exposures to the API.

        On failure, re-queues exposures (with a limit to avoid memory issues).
        """
        if not self._buffer:
            return

        batch = self._buffer[:]
        self._buffer.clear()

        try:
            response = requests.post(
                self._endpoint,
                json={
                    "exposures": [
                        {
                            "userId": e.user_id,
                            "experimentId": e.experiment_id,
                            "variantKey": e.variant_key,
                            "timestamp": e.timestamp,
                        }
                        for e in batch
                    ]
                },
                headers={
                    "Authorization": self._basic_auth_header(),
                    "Content-Type": "application/json",
                },
                timeout=self._request_timeout,
            )
            response.raise_for_status()
            logger.debug(f"Flushed {len(batch)} exposures")
        except requests.exceptions.RequestException as e:
            logger.warning(f"Failed to send exposures: {e}")
            # Re-queue failed exposures (with limit to avoid unbounded growth)
            capacity = MAX_REQUEUE_SIZE - len(self._buffer)
            requeue = batch[:capacity]
            self._buffer.extend(requeue)
            if len(batch) > len(requeue):
                logger.warning(f"Dropped {len(batch) - len(requeue)} exposures due to buffer limit")
        except Exception as e:
            logger.error(f"Unexpected error sending exposures: {e}")
            capacity = MAX_REQUEUE_SIZE - len(self._buffer)
            requeue = batch[:capacity]
            self._buffer.extend(requeue)
            if len(batch) > len(requeue):
                logger.warning(f"Dropped {len(batch) - len(requeue)} exposures due to buffer limit")
