"""
Climeter HTTP client — handles async event submission with local buffering.

Zero stdlib dependencies: uses threading, urllib, json, os only.
"""

import json
import os
import queue
import threading
import time
from collections import deque
from pathlib import Path
from typing import Dict, List, Optional
from urllib import request as urllib_request
from urllib.error import URLError

DEFAULT_BASE_URL = "https://api.climeter.ai"
DEFAULT_FLUSH_INTERVAL = 5.0   # seconds
DEFAULT_BATCH_SIZE = 50
DEFAULT_MAX_QUEUE = 10_000
MAX_OFFLINE_BUFFER = 100       # max events held in memory when offline
OFFLINE_BUFFER_PATH = Path.home() / ".climeter" / "offline_buffer.jsonl"


class ClimeterClient:
    """
    Async, fire-and-forget HTTP client for Climeter.

    Events are queued in-memory and flushed in background batches.
    If network is unavailable, up to MAX_OFFLINE_BUFFER events are kept
    in a memory buffer and retried on the next successful flush.
    """

    def __init__(
        self,
        api_key: str,
        base_url: str = DEFAULT_BASE_URL,
        flush_interval: float = DEFAULT_FLUSH_INTERVAL,
        batch_size: int = DEFAULT_BATCH_SIZE,
        max_queue: int = DEFAULT_MAX_QUEUE,
    ):
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.flush_interval = flush_interval
        self.batch_size = batch_size
        self._queue: queue.Queue = queue.Queue(maxsize=max_queue)
        self._offline_buffer: deque = deque(maxlen=MAX_OFFLINE_BUFFER)
        self._offline_lock = threading.Lock()
        self._shutdown = threading.Event()
        self._thread = threading.Thread(
            target=self._flush_loop, daemon=True, name="climeter-flush"
        )
        self._thread.start()

    def enqueue(self, event: dict) -> None:
        """Add an event to the async queue. Never blocks the caller."""
        try:
            self._queue.put_nowait(event)
        except queue.Full:
            # Drop oldest to make room (ring-buffer behaviour)
            try:
                self._queue.get_nowait()
                self._queue.put_nowait(event)
            except (queue.Empty, queue.Full):
                pass

    def flush(self) -> None:
        """Force-flush pending events. Useful at process exit."""
        # First retry any buffered offline events
        self._retry_offline_buffer()

        batch = []
        while not self._queue.empty():
            try:
                batch.append(self._queue.get_nowait())
            except queue.Empty:
                break
            if len(batch) >= self.batch_size:
                self._send_batch(batch)
                batch = []
        if batch:
            self._send_batch(batch)

    def shutdown(self) -> None:
        """Gracefully shut down: flush remaining events then stop."""
        self._shutdown.set()
        self.flush()
        self._thread.join(timeout=10)

    # ------------------------------------------------------------------
    # Internal
    # ------------------------------------------------------------------

    def _flush_loop(self) -> None:
        """Background thread: drains queue every flush_interval seconds."""
        while not self._shutdown.is_set():
            time.sleep(self.flush_interval)
            # Retry buffered events first
            self._retry_offline_buffer()

            batch = []
            while not self._queue.empty() and len(batch) < self.batch_size:
                try:
                    batch.append(self._queue.get_nowait())
                except queue.Empty:
                    break
            if batch:
                self._send_batch(batch)

    def _send_batch(self, events: List[dict]) -> None:
        """HTTP POST a batch to /v1/events/batch."""
        payload = json.dumps({"events": events}).encode("utf-8")
        url = f"{self.base_url}/v1/events/batch"
        req = urllib_request.Request(
            url,
            data=payload,
            headers={
                "Content-Type": "application/json",
                "X-API-Key": self.api_key,
                "User-Agent": "climeter-python/0.1.0",
            },
            method="POST",
        )
        try:
            with urllib_request.urlopen(req, timeout=10) as resp:
                if resp.status not in (200, 202):
                    self._buffer_offline(events)
        except Exception:
            self._buffer_offline(events)

    def _buffer_offline(self, events: List[dict]) -> None:
        """Hold failed events in the bounded in-memory deque (max 100)."""
        with self._offline_lock:
            for event in events:
                self._offline_buffer.append(event)  # deque(maxlen=100) auto-drops oldest

    def _retry_offline_buffer(self) -> None:
        """Attempt to resend any buffered events."""
        with self._offline_lock:
            if not self._offline_buffer:
                return
            batch = list(self._offline_buffer)
            self._offline_buffer.clear()

        # Send outside the lock; failures re-buffer automatically
        for i in range(0, len(batch), self.batch_size):
            self._send_batch(batch[i: i + self.batch_size])

    # ------------------------------------------------------------------
    # Convenience for diagnostics
    # ------------------------------------------------------------------

    @property
    def buffered_count(self) -> int:
        with self._offline_lock:
            return len(self._offline_buffer)

    def queue_size(self) -> int:
        """Return the number of events currently in the queue."""
        return self._queue.qsize()
