"""Main client classes for stdiobus."""

import asyncio
import json
import random
import string
import sys
import time
import uuid
from typing import Any, Callable, Optional

from stdiobus.types import (
    BusState,
    BackendMode,
    BusStats,
    BusOptions,
    DockerOptions,
    MessageHandler,
)
from stdiobus.errors import (
    InvalidArgumentError,
    InvalidStateError,
    TimeoutError,
    TransportError,
    error_from_code,
)
from stdiobus.backends.base import Backend
from stdiobus.backends.docker import DockerBackend


def generate_client_session_id() -> str:
    """Generate a unique client session ID for stdio-bus routing."""
    timestamp = int(time.time() * 1000)
    random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
    return f"client-{timestamp}-{random_suffix}"


def _resolve_backend(mode: BackendMode, config_path: str, docker_options: Optional[DockerOptions] = None) -> Backend:
    """Determine and create the appropriate backend."""
    import platform
    
    if mode == BackendMode.DOCKER:
        return DockerBackend(config_path, docker_options)
    
    if mode == BackendMode.NATIVE:
        # Try to import native backend
        try:
            from stdiobus.backends.native import NativeBackend, is_native_available
            if not is_native_available():
                raise ImportError("Native bindings not built")
            return NativeBackend(config_path)
        except ImportError as e:
            raise InvalidArgumentError(
                f"Native backend not available: {e}. "
                "Use backend='docker' or build native bindings."
            )
    
    # Auto mode
    system = platform.system().lower()
    
    if system == "windows":
        # Windows: always use Docker
        return DockerBackend(config_path, docker_options)
    
    # Unix: try native first, fall back to docker
    try:
        from stdiobus.backends.native import NativeBackend, is_native_available
        if is_native_available():
            return NativeBackend(config_path)
    except ImportError:
        pass
    
    # Fall back to Docker
    return DockerBackend(config_path, docker_options)


class AsyncStdioBus:
    """Async stdio_bus client.
    
    Example:
        async with AsyncStdioBus(config_path='./config.json') as bus:
            result = await bus.request('tools/list', {})
    """
    
    def __init__(
        self,
        config_path: str,
        *,
        backend: BackendMode | str = BackendMode.AUTO,
        timeout_ms: int = 30000,
        docker: Optional[DockerOptions] = None,
    ):
        """Create a new AsyncStdioBus instance.
        
        Args:
            config_path: Path to JSON configuration file.
            backend: Backend mode: 'auto', 'native', or 'docker'.
            timeout_ms: Default request timeout in milliseconds.
            docker: Docker backend options.
        """
        if not config_path:
            raise InvalidArgumentError("config_path is required")
        
        if isinstance(backend, str):
            backend = BackendMode(backend)
        
        self._config_path = config_path
        self._backend_mode = backend
        self._timeout_ms = timeout_ms
        self._docker_options = docker
        self._backend: Optional[Backend] = None
        self._message_handlers: list[MessageHandler] = []
        self._pending_requests: dict[str, asyncio.Future[Any]] = {}
        
        # Create backend using resolver
        self._backend = _resolve_backend(backend, config_path, docker)
        
        # Register internal message handler
        self._backend.on_message(self._handle_message)
    
    def _handle_message(self, message: str) -> None:
        """Handle incoming message."""
        # Call user handlers
        for handler in self._message_handlers:
            try:
                handler(message)
            except Exception as e:
                print(f"[stdiobus] Handler error: {e}", file=sys.stderr)
        
        # Handle pending requests
        try:
            data = json.loads(message)
            msg_id = data.get("id")
            if msg_id and msg_id in self._pending_requests:
                future = self._pending_requests.pop(msg_id)
                if not future.done():
                    if "error" in data:
                        error = data["error"]
                        future.set_exception(
                            error_from_code(
                                error.get("code", 99),
                                error.get("message", "Unknown error"),
                                error.get("data"),
                            )
                        )
                    else:
                        future.set_result(data.get("result"))
        except json.JSONDecodeError:
            pass
    
    def on_message(self, handler: MessageHandler) -> None:
        """Register a message handler.
        
        Args:
            handler: Callback function receiving message string.
        """
        self._message_handlers.append(handler)
    
    async def start(self) -> None:
        """Start the bus and spawn worker processes."""
        if self._backend is None:
            raise InvalidStateError("Bus not initialized")
        await self._backend.start()
    
    async def stop(self, timeout_sec: float = 30.0) -> None:
        """Stop the bus gracefully.
        
        Args:
            timeout_sec: Maximum time to wait for workers to exit.
        """
        if self._backend is None:
            return
        await self._backend.stop(timeout_sec)
    
    def send(self, message: str) -> bool:
        """Send a raw JSON-RPC message to workers.
        
        Args:
            message: JSON-RPC message string.
            
        Returns:
            True if message was queued successfully.
        """
        if self._backend is None:
            return False
        return self._backend.send(message)
    
    async def request(
        self,
        method: str,
        params: Optional[dict[str, Any]] = None,
        *,
        timeout_ms: Optional[int] = None,
        session_id: Optional[str] = None,
    ) -> Any:
        """Send a JSON-RPC request and wait for response.
        
        Args:
            method: RPC method name.
            params: Method parameters.
            timeout_ms: Request timeout in milliseconds.
            session_id: Session ID for routing.
            
        Returns:
            Response result.
            
        Raises:
            TimeoutError: If request times out.
            TransportError: If send fails.
        """
        if self._backend is None or not self._backend.is_running():
            raise InvalidStateError("Bus not running")
        
        msg_id = str(uuid.uuid4())
        timeout = (timeout_ms or self._timeout_ms) / 1000.0
        
        # Build request
        request: dict[str, Any] = {
            "jsonrpc": "2.0",
            "id": msg_id,
            "method": method,
        }
        if params:
            request["params"] = params
        
        # sessionId goes at TOP LEVEL for stdio-bus routing
        if session_id:
            request["sessionId"] = session_id
        
        # Create future for response
        loop = asyncio.get_event_loop()
        future: asyncio.Future[Any] = loop.create_future()
        self._pending_requests[msg_id] = future
        
        try:
            # Send request
            if not self.send(json.dumps(request)):
                raise TransportError("Failed to send message")
            
            # Wait for response
            return await asyncio.wait_for(future, timeout=timeout)
            
        except asyncio.TimeoutError:
            self._pending_requests.pop(msg_id, None)
            raise TimeoutError(f"Request timeout: {method}")
        except Exception:
            self._pending_requests.pop(msg_id, None)
            raise
    
    async def notify(
        self,
        method: str,
        params: Optional[dict[str, Any]] = None,
    ) -> None:
        """Send a JSON-RPC notification (no response expected).
        
        Args:
            method: RPC method name.
            params: Method parameters.
        """
        notification: dict[str, Any] = {
            "jsonrpc": "2.0",
            "method": method,
        }
        if params:
            notification["params"] = params
        
        self.send(json.dumps(notification))
    
    def get_state(self) -> BusState:
        """Get current bus state."""
        if self._backend is None:
            return BusState.STOPPED
        return self._backend.get_state()
    
    def get_stats(self) -> BusStats:
        """Get bus statistics."""
        if self._backend is None:
            return BusStats()
        return self._backend.get_stats()
    
    def get_backend_type(self) -> str:
        """Get the backend type being used."""
        from stdiobus.backends.native import NativeBackend
        from stdiobus.backends.docker import DockerBackend
        
        if isinstance(self._backend, NativeBackend):
            return "native"
        elif isinstance(self._backend, DockerBackend):
            return "docker"
        return "unknown"
    
    def is_running(self) -> bool:
        """Check if bus is running."""
        return self.get_state() == BusState.RUNNING
    
    def destroy(self) -> None:
        """Destroy the bus and release all resources."""
        if self._backend:
            self._backend.destroy()
            self._backend = None
    
    async def __aenter__(self) -> "AsyncStdioBus":
        """Async context manager entry."""
        await self.start()
        return self
    
    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """Async context manager exit."""
        await self.stop()
        self.destroy()


class StdioBus:
    """Synchronous stdio_bus client.
    
    Wraps AsyncStdioBus for synchronous usage.
    
    Example:
        with StdioBus(config_path='./config.json') as bus:
            result = bus.request('tools/list', {})
    """
    
    def __init__(
        self,
        config_path: str,
        *,
        backend: BackendMode | str = BackendMode.AUTO,
        timeout_ms: int = 30000,
        docker: Optional[DockerOptions] = None,
    ):
        """Create a new StdioBus instance.
        
        Args:
            config_path: Path to JSON configuration file.
            backend: Backend mode: 'auto', 'native', or 'docker'.
            timeout_ms: Default request timeout in milliseconds.
            docker: Docker backend options.
        """
        self._async_bus = AsyncStdioBus(
            config_path,
            backend=backend,
            timeout_ms=timeout_ms,
            docker=docker,
        )
        self._loop: Optional[asyncio.AbstractEventLoop] = None
    
    def _get_loop(self) -> asyncio.AbstractEventLoop:
        """Get or create event loop."""
        if self._loop is None or self._loop.is_closed():
            try:
                self._loop = asyncio.get_running_loop()
            except RuntimeError:
                self._loop = asyncio.new_event_loop()
        return self._loop
    
    def _run(self, coro: Any) -> Any:
        """Run coroutine synchronously."""
        loop = self._get_loop()
        return loop.run_until_complete(coro)
    
    def on_message(self, handler: MessageHandler) -> None:
        """Register a message handler."""
        self._async_bus.on_message(handler)
    
    def start(self) -> None:
        """Start the bus."""
        self._run(self._async_bus.start())
    
    def stop(self, timeout_sec: float = 30.0) -> None:
        """Stop the bus."""
        self._run(self._async_bus.stop(timeout_sec))
    
    def send(self, message: str) -> bool:
        """Send a message."""
        return self._async_bus.send(message)
    
    def request(
        self,
        method: str,
        params: Optional[dict[str, Any]] = None,
        *,
        timeout_ms: Optional[int] = None,
        session_id: Optional[str] = None,
    ) -> Any:
        """Send request and wait for response."""
        return self._run(
            self._async_bus.request(
                method,
                params,
                timeout_ms=timeout_ms,
                session_id=session_id,
            )
        )
    
    def notify(
        self,
        method: str,
        params: Optional[dict[str, Any]] = None,
    ) -> None:
        """Send notification."""
        self._run(self._async_bus.notify(method, params))
    
    def get_state(self) -> BusState:
        """Get current state."""
        return self._async_bus.get_state()
    
    def get_stats(self) -> BusStats:
        """Get statistics."""
        return self._async_bus.get_stats()
    
    def get_backend_type(self) -> str:
        """Get backend type."""
        return self._async_bus.get_backend_type()
    
    def is_running(self) -> bool:
        """Check if running."""
        return self._async_bus.is_running()
    
    def destroy(self) -> None:
        """Destroy and release resources."""
        self._async_bus.destroy()
        if self._loop and not self._loop.is_closed():
            self._loop.close()
            self._loop = None
    
    def __enter__(self) -> "StdioBus":
        """Context manager entry."""
        self.start()
        return self
    
    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """Context manager exit."""
        self.stop()
        self.destroy()
