"""Async gRPC channel reuse with a lazy optional dependency."""

from __future__ import annotations

import asyncio
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any


class GrpcDependencyError(RuntimeError):
    """Raised when a gRPC connector is used without the grpc extra."""


@dataclass(frozen=True, slots=True)
class GrpcChannelSettings:
    target: str
    tls: bool = False
    root_certificates: Path | None = None
    options: tuple[tuple[str, int | str], ...] = field(default_factory=tuple)


class GrpcChannelPool:
    """Create one ``grpc.aio.Channel`` per named endpoint.

    The pool must be created and used from a single asyncio event-loop thread,
    matching the threading requirements of ``grpc.aio`` objects.
    """

    def __init__(self) -> None:
        self._channels: dict[str, Any] = {}
        self._settings: dict[str, GrpcChannelSettings] = {}
        self._lock = asyncio.Lock()

    async def get(self, endpoint_id: str, settings: GrpcChannelSettings) -> Any:
        async with self._lock:
            existing = self._channels.get(endpoint_id)
            if existing is not None:
                if self._settings[endpoint_id] != settings:
                    raise ValueError(
                        f"gRPC endpoint {endpoint_id!r} requested with conflicting settings"
                    )
                return existing

            try:
                import grpc
            except ImportError as exc:
                raise GrpcDependencyError(
                    "gRPC support is not installed; install cmvr-edge-ai[grpc]"
                ) from exc

            if settings.tls:
                roots = (
                    settings.root_certificates.read_bytes()
                    if settings.root_certificates is not None
                    else None
                )
                credentials = grpc.ssl_channel_credentials(root_certificates=roots)
                channel = grpc.aio.secure_channel(
                    settings.target, credentials, options=settings.options
                )
            else:
                channel = grpc.aio.insecure_channel(
                    settings.target, options=settings.options
                )

            self._channels[endpoint_id] = channel
            self._settings[endpoint_id] = settings
            return channel

    async def close(self, grace_s: float = 1.0) -> None:
        async with self._lock:
            channels = tuple(self._channels.values())
            self._channels.clear()
            self._settings.clear()
        await asyncio.gather(
            *(channel.close(grace=grace_s) for channel in channels),
            return_exceptions=True,
        )
