"""Application-level ownership for pipelines and shared network clients."""

from __future__ import annotations

import asyncio
from collections.abc import Iterable
from concurrent.futures import ThreadPoolExecutor
from enum import Enum

from cmvr_edge_ai.compiler import (
    CompiledPipeline,
    PipelineCompileError,
    compile_pipeline,
    enabled_pipeline_ids,
)
from cmvr_edge_ai.config import AppConfig
from cmvr_edge_ai.connectors import register_connector_plugins
from cmvr_edge_ai.detection import (
    DetectionModelRegistry,
    register_builtin_detection_models,
    register_detection_plugins,
)
from cmvr_edge_ai.plugins import PluginRegistry, register_builtin_plugins
from cmvr_edge_ai.transports.grpc import GrpcChannelPool
from cmvr_edge_ai.transports.http import HttpClientPool


class ApplicationState(str, Enum):
    CREATED = "created"
    STARTING = "starting"
    RUNNING = "running"
    STOPPING = "stopping"
    STOPPED = "stopped"


def create_default_model_registry(
    *, discover_entry_points: bool = True
) -> DetectionModelRegistry:
    """Create the artifact registry used by generic detection nodes."""

    registry = DetectionModelRegistry()
    register_builtin_detection_models(registry)
    if discover_entry_points:
        registry.load_entry_points()
    return registry


def create_default_registry(
    *,
    discover_entry_points: bool = True,
    model_registry: DetectionModelRegistry | None = None,
) -> PluginRegistry:
    registry = PluginRegistry()
    register_builtin_plugins(registry)
    register_connector_plugins(registry)
    detection_models = model_registry or create_default_model_registry(
        discover_entry_points=discover_entry_points
    )
    register_detection_plugins(registry, detection_models)
    if discover_entry_points:
        registry.load_entry_points()
    return registry


def validate_application(
    config: AppConfig,
    registry: PluginRegistry,
    pipeline_ids: Iterable[str] | None = None,
) -> tuple[CompiledPipeline, ...]:
    selected = _select_pipeline_ids(config, pipeline_ids)
    return tuple(
        compile_pipeline(config, pipeline_id, registry) for pipeline_id in selected
    )


class EdgeAIApplication:
    """Own shared transports and one or more local pipeline runtimes."""

    def __init__(self, config: AppConfig, registry: PluginRegistry) -> None:
        self.config = config
        self.registry = registry
        self.state = ApplicationState.CREATED
        self.grpc_channels = GrpcChannelPool()
        self.http_clients = HttpClientPool()
        self._compiled: dict[str, CompiledPipeline] = {}
        self._executor: ThreadPoolExecutor | None = None
        self._lifecycle_lock = asyncio.Lock()

    @property
    def pipelines(self) -> tuple[str, ...]:
        return tuple(self._compiled)

    async def start(self, pipeline_ids: Iterable[str] | None = None) -> None:
        async with self._lifecycle_lock:
            if self.state is not ApplicationState.CREATED:
                raise RuntimeError(
                    f"cannot start application in state {self.state.value}"
                )
            selected = _select_pipeline_ids(self.config, pipeline_ids)
            self.state = ApplicationState.STARTING
            try:
                self._executor = ThreadPoolExecutor(
                    max_workers=self.config.runtime.thread_workers,
                    thread_name_prefix="cmvr-edge-ai",
                )
                metadata = {
                    "endpoints": self.config.endpoints,
                    "grpc_channel_pool": self.grpc_channels,
                    "http_client_pool": self.http_clients,
                    "thread_executor": self._executor,
                }
                for pipeline_id in selected:
                    compiled = compile_pipeline(
                        self.config,
                        pipeline_id,
                        self.registry,
                        metadata=metadata,
                    )
                    self._compiled[pipeline_id] = compiled
                for compiled in self._compiled.values():
                    await compiled.runtime.start()
            except BaseException:
                await self._stop_unlocked(graceful=False)
                raise
            self.state = ApplicationState.RUNNING

    async def wait(self) -> None:
        if self.state is not ApplicationState.RUNNING:
            if self.state is ApplicationState.STOPPED:
                return
            raise RuntimeError("application has not been started")
        await asyncio.gather(
            *(compiled.runtime.wait() for compiled in self._compiled.values())
        )

    async def stop(self, *, graceful: bool = True) -> None:
        async with self._lifecycle_lock:
            await self._stop_unlocked(graceful=graceful)

    async def _stop_unlocked(self, *, graceful: bool) -> None:
        if self.state is ApplicationState.STOPPED:
            return
        self.state = ApplicationState.STOPPING
        await asyncio.gather(
            *(
                compiled.runtime.stop(graceful=graceful)
                for compiled in self._compiled.values()
            ),
            return_exceptions=True,
        )
        await asyncio.gather(
            self.grpc_channels.close(),
            self.http_clients.close(),
            return_exceptions=True,
        )
        if self._executor is not None:
            self._executor.shutdown(wait=False, cancel_futures=True)
            self._executor = None
        self.state = ApplicationState.STOPPED

    async def health(self) -> dict[str, object]:
        reports = {
            pipeline_id: await compiled.runtime.health()
            for pipeline_id, compiled in self._compiled.items()
        }
        return {"state": self.state.value, "pipelines": reports}


def _select_pipeline_ids(
    config: AppConfig, pipeline_ids: Iterable[str] | None
) -> tuple[str, ...]:
    if pipeline_ids is None:
        selected = enabled_pipeline_ids(config)
    else:
        selected = tuple(dict.fromkeys(pipeline_ids))
        unknown = sorted(set(selected) - set(config.pipelines))
        if unknown:
            raise PipelineCompileError(f"unknown pipeline(s): {', '.join(unknown)}")
        disabled = sorted(
            pipeline_id
            for pipeline_id in selected
            if not config.pipelines[pipeline_id].enabled
        )
        if disabled:
            raise PipelineCompileError(
                f"selected pipeline(s) are disabled: {', '.join(disabled)}"
            )
    if not selected:
        raise PipelineCompileError("no enabled pipelines selected")
    return selected


__all__ = [
    "ApplicationState",
    "EdgeAIApplication",
    "create_default_model_registry",
    "create_default_registry",
    "validate_application",
]
