from __future__ import annotations

import sys
import threading
import time
from typing import Callable, List

WORKER_INTERVAL = 0.01
WORKER_SHUTDOWN_TIMEOUT = 5.0
_ADAPTIVE_THRESHOLD = 0.5


class BackgroundWorker(threading.Thread):
    """Daemon thread that drains the LogQueue and dispatches batches.

    FIX #4 — batch timestamp:
        worker stamps time.time() ONCE per drain cycle and fills it into
        every record whose timestamp is None. This moves time.time() out
        of the producer hot path (was called 187k/sec) into the worker
        (called ~100/sec). Trades per-record precision for throughput.
        Records logged in the same 10ms window share one timestamp —
        acceptable for all ML training loop use cases.

    Compatible with LogQueue and ShardedLogQueue — never accesses internal
    queue attributes except _event and _maxsize.
    """

    def __init__(
        self,
        queue,
        handlers: List[Callable],
        interval: float = WORKER_INTERVAL,
    ) -> None:
        super().__init__(daemon=True, name="pulselog-worker")
        self._queue = queue
        self._handlers = [h for h in handlers if h is not None]
        self._base_interval = max(interval, 0.005)
        self._interval = self._base_interval
        self._stop_event = threading.Event()

    def run(self) -> None:
        while not self._stop_event.is_set():
            self._queue._event.wait(timeout=self._interval)
            batch = self._queue.drain()
            if batch:
                self._stamp(batch)
                self._dispatch(batch)
                self._adapt_interval()

        # Final drain after stop signal.
        batch = self._queue.drain()
        if batch:
            self._stamp(batch)
            self._dispatch(batch)

    def shutdown(self, timeout: float = WORKER_SHUTDOWN_TIMEOUT) -> None:
        self._stop_event.set()
        self._queue._event.set()
        self.join(timeout=timeout)

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

    @staticmethod
    def _stamp(batch: list) -> None:
        """FIX #4 — fill timestamp for records that deferred it.

        One time.time() call per drain cycle instead of one per record.
        Only stamps records where timestamp is None — records that already
        carry a timestamp (e.g. checkpoints, dividers) are left untouched.
        """
        t = time.time()
        for record in batch:
            if record.timestamp is None:
                record.timestamp = t

    def _adapt_interval(self) -> None:
        """Halve sleep when queue is under pressure, restore when calm.

        Uses public len() and _maxsize — works with both queue types.
        """
        queue_len  = len(self._queue)
        capacity   = self._queue._maxsize or 1
        fill_ratio = queue_len / capacity

        if fill_ratio >= _ADAPTIVE_THRESHOLD:
            self._interval = max(self._base_interval / 2, 0.005)
        else:
            self._interval = self._base_interval

    def _dispatch(self, batch: list) -> None:
        for handler in self._handlers:
            try:
                handler(batch)
            except Exception as exc:
                sys.stderr.write(
                    f"[pulselog] Handler {handler!r} raised: {exc}\n"
                )