"""Async event pipeline - buffers and batch-sends events to Obol cloud.

Performance: <1 microsecond per record() call (just appends to deque).
Flushing happens in background thread.
"""
import json
import os
import sys
import threading
import time
from collections import deque
from typing import Optional

from obol.api import ObolAPI

_OVERFLOW_WARN_THRESHOLD = 0.8  # Warn at 80% capacity


class EventPipeline:
    """Async event pipeline that batches events for efficient API calls."""

    def __init__(
        self,
        api: ObolAPI,
        agent_id: int,
        session_id: str,
        flush_interval: float = 5.0,
        flush_threshold: int = 100,
        max_buffer: int = 10000,
    ):
        self.api = api
        self.agent_id = agent_id
        self.session_id = session_id
        self.flush_interval = flush_interval
        self.flush_threshold = flush_threshold
        self._max_buffer = max_buffer

        self._buffer = deque(maxlen=max_buffer)
        self._lock = threading.Lock()
        self._running = False
        self._thread = None
        self._overflow_warned = False
        self._flush_failures = 0

    def start(self):
        """Start background flush thread."""
        self._running = True
        self._thread = threading.Thread(target=self._flush_loop, daemon=True)
        self._thread.start()

    def stop(self):
        """Stop background flush thread."""
        self._running = False
        if self._thread:
            self._thread.join(timeout=5)

    def record(self, event: dict):
        """Add event to buffer. Non-blocking, <1us."""
        event["agent_id"] = self.agent_id
        event["session_id"] = self.session_id

        # Warn if buffer is approaching capacity
        buf_len = len(self._buffer)
        if buf_len >= self._max_buffer * _OVERFLOW_WARN_THRESHOLD and not self._overflow_warned:
            self._overflow_warned = True
            print(f"[obol] Warning: event buffer at {buf_len}/{self._max_buffer} "
                  f"({buf_len * 100 // self._max_buffer}%). Oldest events may be dropped.",
                  file=sys.stderr)

        self._buffer.append(event)

        # Trigger flush if threshold reached
        if len(self._buffer) >= self.flush_threshold:
            self._flush_async()

    def flush(self):
        """Force flush all buffered events."""
        self._do_flush()

    def _flush_loop(self):
        """Background thread: periodically flush buffer."""
        while self._running:
            time.sleep(self.flush_interval)
            if self._buffer:
                self._do_flush()

    def _flush_async(self):
        """Non-blocking flush trigger."""
        threading.Thread(target=self._do_flush, daemon=True).start()

    def _do_flush(self):
        """Send all buffered events to API."""
        if not self._buffer:
            return

        with self._lock:
            events = list(self._buffer)
            self._buffer.clear()

        if not events:
            return

        try:
            # Batch send
            if len(events) == 1:
                self.api.record_event(**events[0])
            else:
                self.api.record_events_batch(events)
            self._flush_failures = 0
            self._overflow_warned = False  # Reset warning after successful flush
        except Exception:
            self._flush_failures += 1
            if self._flush_failures <= 3:
                # Put events back for retry (best effort)
                for event in events:
                    self._buffer.appendleft(event)
            else:
                # After 3 consecutive failures, write to local fallback
                self._write_failed_events(events)

    def _write_failed_events(self, events: list):
        """Write events to local fallback file for later retry via `obol retry`."""
        try:
            fallback = os.path.expanduser("~/.obol/failed_events.jsonl")
            os.makedirs(os.path.dirname(fallback), exist_ok=True)
            with open(fallback, "a") as f:
                for event in events:
                    f.write(json.dumps(event) + "\n")
        except Exception:
            pass  # Never let logging break the agent

    @property
    def pending_count(self) -> int:
        return len(self._buffer)
