"""
HTTP client for communicating with Visibe backend.
"""
import logging
import threading
import requests
from typing import Dict, Any, List, Optional

logger = logging.getLogger("visibe")


class APIClient:
    """Handles HTTP communication with Visibe backend."""

    def __init__(
        self,
        api_url: str,
        api_key: Optional[str] = None,
        timeout: int = 10
    ):
        self.api_url = api_url.rstrip('/')
        self.api_key = api_key
        self.timeout = timeout

    def _headers(self) -> Dict[str, str]:
        headers = {"Content-Type": "application/json"}
        if self.api_key:
            headers["Authorization"] = f"Bearer {self.api_key}"
        return headers

    def _request(self, method: str, url: str, json_data: Dict[str, Any],
                 label: str) -> bool:
        """Shared request logic with error handling."""
        try:
            response = requests.request(
                method, url, json=json_data,
                headers=self._headers(), timeout=self.timeout
            )
            response.raise_for_status()
            logger.info(label)
            return True
        except requests.exceptions.HTTPError as e:
            logger.warning(f"{label} — HTTP error: {e}")
            try:
                logger.debug(f"Error detail: {response.json()}")
            except Exception:
                logger.debug(f"Error body: {response.text}")
            return False
        except requests.exceptions.ConnectionError as e:
            logger.warning(f"{label} — connection error: {e}")
            return False
        except requests.exceptions.Timeout:
            logger.warning(f"{label} — timed out after {self.timeout}s")
            return False
        except Exception as e:
            logger.warning(f"{label} — error: {e}")
            return False

    def create_trace(self, trace_data: Dict[str, Any]) -> bool:
        """POST /api/traces — lightweight trace header."""
        return self._request(
            "POST",
            f"{self.api_url}/api/traces",
            trace_data,
            f"Trace created: {trace_data.get('trace_id', 'unknown')}",
        )

    def send_span(self, trace_id: str, span_data: Dict[str, Any]) -> bool:
        """POST /api/traces/{trace_id}/spans — single span (kept for direct use)."""
        return self._request(
            "POST",
            f"{self.api_url}/api/traces/{trace_id}/spans",
            span_data,
            f"Span sent: {span_data.get('span_id', 'unknown')} → trace {trace_id}",
        )

    def send_spans_batch(self, trace_id: str, spans: List[Dict[str, Any]]) -> bool:
        """POST /api/traces/{trace_id}/spans/batch — batch of spans."""
        return self._request(
            "POST",
            f"{self.api_url}/api/traces/{trace_id}/spans/batch",
            {"spans": spans},
            f"Batch sent: {len(spans)} spans → trace {trace_id}",
        )

    def complete_trace(self, trace_id: str, summary: Dict[str, Any]) -> bool:
        """PATCH /api/traces/{trace_id} — final summary."""
        return self._request(
            "PATCH",
            f"{self.api_url}/api/traces/{trace_id}",
            summary,
            f"Trace completed: {trace_id}",
        )


class SpanBatcher:
    """Buffers spans and sends them in batches via a background thread.

    - add() is non-blocking — just appends to buffer
    - Background thread flushes every `flush_interval` seconds
    - Buffer also auto-flushes when it hits `batch_size`
    - flush() is blocking — sends everything remaining (call before complete_trace)
    """

    def __init__(self, api_client: APIClient, batch_size: int = 50,
                 flush_interval: float = 2.0):
        self._api_client = api_client
        self._batch_size = batch_size
        self._flush_interval = flush_interval
        self._buffer: List[tuple] = []  # [(trace_id, span_data), ...]
        self._lock = threading.Lock()
        self._flush_event = threading.Event()
        self._stopped = False
        self._thread = threading.Thread(target=self._worker, daemon=True)
        self._thread.start()

    def add(self, trace_id: str, span_data: Dict[str, Any]):
        """Add a span to the buffer. Non-blocking."""
        with self._lock:
            self._buffer.append((trace_id, span_data))
            if len(self._buffer) >= self._batch_size:
                self._flush_event.set()

    def flush(self):
        """Flush all buffered spans immediately. Blocking."""
        with self._lock:
            batch = self._buffer[:]
            self._buffer = []
        self._send(batch)

    def shutdown(self):
        """Flush remaining spans and stop the background thread."""
        self._stopped = True
        self._flush_event.set()
        self.flush()
        self._thread.join(timeout=5)

    def _send(self, batch: List[tuple]):
        if not batch:
            return
        # Group by trace_id
        by_trace: Dict[str, List[Dict]] = {}
        for trace_id, span in batch:
            by_trace.setdefault(trace_id, []).append(span)
        for trace_id, spans in by_trace.items():
            self._api_client.send_spans_batch(trace_id, spans)

    def _worker(self):
        while not self._stopped:
            self._flush_event.wait(timeout=self._flush_interval)
            self._flush_event.clear()
            with self._lock:
                batch = self._buffer[:]
                self._buffer = []
            self._send(batch)
