"""
Emergent Language Client - Fire-and-forget semantic compression.

Auto-detects optimal mode:
- SIDECAR: If collector on localhost, sends mini-batches to it
- DIRECT: Otherwise, batches in-process and sends to API

Performance:
- Direct mode: ~8,000 msg/s
- Sidecar mode: ~9,700 msg/s (with local collector)
"""

import asyncio
import time
import httpx
from typing import Dict, List, Optional
from dataclasses import dataclass
from enum import Enum


class Mode(Enum):
    SIDECAR = "sidecar"
    DIRECT = "direct"


@dataclass
class Config:
    """Client configuration."""

    # Translator API
    translator_url: str = "https://emergent-language.fly.dev"
    auth_token: str = "eudaimonia-translator-demo"

    # Collector (for sidecar mode)
    collector_url: str = "http://localhost:8080"

    # Batching
    batch_size: int = 75
    mini_batch_size: int = 25
    flush_interval_ms: int = 50
    num_workers: int = 4

    # Auto-detection
    auto_detect: bool = True


@dataclass
class Stats:
    """Client statistics."""
    messages_queued: int = 0
    messages_sent: int = 0
    batches_sent: int = 0
    bytes_saved: int = 0
    errors: int = 0
    mode: str = "unknown"


class EmergentClient:
    """
    High-performance client for Emergent Language translation.

    Usage:
        async with EmergentClient() as client:
            await client.send({"task": "analyze", "data": "market"})

    The client auto-detects if a collector sidecar is available:
    - If yes: Sends mini-batches to collector (sidecar mode)
    - If no: Batches in-process and sends to API (direct mode)
    """

    def __init__(self, config: Optional[Config] = None):
        """
        Initialize client.

        Args:
            config: Optional configuration. Uses defaults if not provided.
        """
        self.config = config or Config()
        self._queue: asyncio.Queue = asyncio.Queue()
        self._running = False
        self._tasks: List[asyncio.Task] = []
        self._mode: Mode = Mode.DIRECT
        self.stats = Stats()

    async def start(self):
        """Start background workers."""
        self._running = True

        # Auto-detect mode
        if self.config.auto_detect:
            collector_available = await self._check_collector()
            self._mode = Mode.SIDECAR if collector_available else Mode.DIRECT
        else:
            self._mode = Mode.DIRECT

        self.stats.mode = self._mode.value

        # Start workers
        worker_fn = self._sidecar_worker if self._mode == Mode.SIDECAR else self._direct_worker
        for i in range(self.config.num_workers):
            task = asyncio.create_task(worker_fn(i))
            self._tasks.append(task)

    async def stop(self):
        """Stop workers and flush remaining messages."""
        self._running = False

        # Wait for queue to drain
        timeout = 10.0
        start = time.time()
        while not self._queue.empty() and (time.time() - start) < timeout:
            await asyncio.sleep(0.05)

        # Cancel workers
        for task in self._tasks:
            task.cancel()
        await asyncio.gather(*self._tasks, return_exceptions=True)
        self._tasks = []

    async def __aenter__(self):
        await self.start()
        return self

    async def __aexit__(self, *args):
        await self.stop()

    async def send(self, data: Dict) -> bool:
        """
        Send a message for translation. Fire-and-forget.

        Args:
            data: Message data (dict)

        Returns:
            True if queued successfully
        """
        if not self._running:
            return False
        await self._queue.put(data)
        self.stats.messages_queued += 1
        return True

    async def send_many(self, messages: List[Dict]) -> int:
        """
        Send multiple messages.

        Args:
            messages: List of message dicts

        Returns:
            Number of messages queued
        """
        count = 0
        for msg in messages:
            if await self.send(msg):
                count += 1
        return count

    @property
    def queue_size(self) -> int:
        """Current queue size."""
        return self._queue.qsize()

    @property
    def mode(self) -> str:
        """Current operating mode."""
        return self._mode.value

    async def _check_collector(self) -> bool:
        """Check if collector is available on localhost."""
        try:
            async with httpx.AsyncClient(timeout=2.0) as client:
                r = await client.get(f"{self.config.collector_url}/health")
                return r.status_code == 200
        except:
            return False

    async def _sidecar_worker(self, worker_id: int):
        """Worker for sidecar mode - mini-batch to collector."""
        async with httpx.AsyncClient(timeout=10.0) as client:
            batch = []

            while self._running or not self._queue.empty():
                try:
                    msg = await asyncio.wait_for(
                        self._queue.get(),
                        timeout=self.config.flush_interval_ms / 1000
                    )
                    batch.append(msg)

                    if len(batch) >= self.config.mini_batch_size:
                        await self._send_to_collector(client, batch)
                        batch = []

                except asyncio.TimeoutError:
                    if batch:
                        await self._send_to_collector(client, batch)
                        batch = []
                except asyncio.CancelledError:
                    if batch:
                        await self._send_to_collector(client, batch)
                    break

    async def _send_to_collector(self, client: httpx.AsyncClient, batch: List[Dict]):
        """Send mini-batch to collector."""
        try:
            await client.post(
                f"{self.config.collector_url}/collect/bulk",
                json={"messages": batch}
            )
            self.stats.messages_sent += len(batch)
            self.stats.batches_sent += 1
        except:
            self.stats.errors += 1
            # Requeue on failure
            for msg in batch:
                await self._queue.put(msg)

    async def _direct_worker(self, worker_id: int):
        """Worker for direct mode - batch and send to API."""
        async with httpx.AsyncClient(timeout=30.0) as client:
            batch = []

            while self._running or not self._queue.empty():
                try:
                    msg = await asyncio.wait_for(
                        self._queue.get(),
                        timeout=self.config.flush_interval_ms / 1000
                    )
                    batch.append(msg)

                    if len(batch) >= self.config.batch_size:
                        await self._send_to_api(client, batch)
                        batch = []

                except asyncio.TimeoutError:
                    if batch:
                        await self._send_to_api(client, batch)
                        batch = []
                except asyncio.CancelledError:
                    if batch:
                        await self._send_to_api(client, batch)
                    break

    async def _send_to_api(self, client: httpx.AsyncClient, batch: List[Dict]):
        """Send batch to translator API."""
        try:
            response = await client.post(
                f"{self.config.translator_url}/translate/batch",
                json={"messages": batch},
                headers={"Authorization": f"Bearer {self.config.auth_token}"}
            )
            if response.status_code == 200:
                result = response.json()
                self.stats.messages_sent += len(batch)
                self.stats.batches_sent += 1
                orig = result.get('original_size', 0)
                comp = result.get('compressed_size', 0)
                self.stats.bytes_saved += (orig - comp)
            else:
                self.stats.errors += 1
        except:
            self.stats.errors += 1


# ═══════════════════════════════════════════════════════════════════════════════
# Module-level convenience API
# ═══════════════════════════════════════════════════════════════════════════════

_default_client: Optional[EmergentClient] = None


async def init(config: Optional[Config] = None) -> EmergentClient:
    """
    Initialize the default client.

    Args:
        config: Optional configuration

    Returns:
        The initialized client
    """
    global _default_client
    _default_client = EmergentClient(config)
    await _default_client.start()
    return _default_client


async def send(data: Dict) -> bool:
    """
    Send a message using the default client.

    Initializes client automatically if needed.
    """
    global _default_client
    if _default_client is None:
        await init()
    return await _default_client.send(data)


async def flush(timeout: float = 10.0):
    """Wait for all queued messages to be sent."""
    if _default_client is None:
        return
    start = time.time()
    while _default_client.queue_size > 0 and (time.time() - start) < timeout:
        await asyncio.sleep(0.05)


async def close():
    """Close the default client."""
    global _default_client
    if _default_client:
        await _default_client.stop()
        _default_client = None
