"""Media server plugin registry.

Single source of truth for which servers exist, what their canonical
names are, and which client class implements each. Replaces the
historic web_server.py pattern of holding 4 separate client globals
that every dispatch site reached individually.

Server-specific dispatch chains in web_server.py (playlist add /
remove / replace, per-server metadata sync, etc.) still hand-branch
on ``active_server == X`` because the work each server does at those
sites is genuinely different — but they reach the per-server CLIENT
through ``engine.client(name)`` (which goes through this registry)
instead of separate globals.

Adding a new server (Subsonic / Emby) = one ``register`` call here +
the new client class.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Callable, Dict, Iterator, List, Optional, Tuple

from utils.logging_config import get_logger

from core.media_server.contract import MediaServerClient

# Eager imports for the same import-order reason the download plugin
# registry uses them (some integration tests inject mock modules into
# sys.modules at collection time; lazy import would bind to the mock).
from core.jellyfin_client import JellyfinClient
from core.navidrome_client import NavidromeClient
from core.plex_client import PlexClient
from core.soulsync_client import SoulSyncClient

logger = get_logger("media_server.registry")


@dataclass(frozen=True)
class ServerSpec:
    """Static descriptor for a media server. ``factory`` is the
    zero-arg callable that builds the client (each server has its
    own setup chain — Plex pulls token from config, Jellyfin reads
    user_id, etc.)."""

    name: str
    factory: Callable[[], MediaServerClient]
    display_name: str
    aliases: Tuple[str, ...] = field(default_factory=tuple)


class MediaServerRegistry:
    """Holds the live client instances + name → instance lookup.

    Two-phase construction (mirrors the download plugin registry):
    1. Specs registered cheaply (just stores callable refs).
    2. ``initialize()`` calls each factory once. Failures captured
       in ``init_failures`` so one broken server doesn't take down
       the orchestrator.
    """

    def __init__(self) -> None:
        self._specs: Dict[str, ServerSpec] = {}
        self._instances: Dict[str, Optional[MediaServerClient]] = {}
        self._init_failures: List[str] = []

    def register(self, spec: ServerSpec) -> None:
        if spec.name in self._specs:
            raise ValueError(f"Server already registered: {spec.name}")
        self._specs[spec.name] = spec

    def initialize(self) -> None:
        for spec in self._specs.values():
            try:
                instance = spec.factory()
                self._instances[spec.name] = instance
            except Exception as exc:
                logger.error("%s media server client failed to initialize: %s", spec.display_name, exc)
                self._init_failures.append(spec.display_name)
                self._instances[spec.name] = None

    def set_instance(self, name: str, instance: Optional[MediaServerClient]) -> None:
        """Stash a pre-built client instance into the registry without
        running the spec's factory. Used by callers (web_server.py at
        boot) that already constructed the per-server clients and want
        the engine to wrap those exact instances rather than build new
        ones. Replaces the old pattern of reaching into ``_instances``
        directly from the engine."""
        self._instances[name] = instance

    @property
    def init_failures(self) -> List[str]:
        return list(self._init_failures)

    def get(self, name: str) -> Optional[MediaServerClient]:
        if not name:
            return None
        if name in self._instances:
            return self._instances[name]
        for spec in self._specs.values():
            if name in spec.aliases:
                return self._instances.get(spec.name)
        return None

    def get_spec(self, name: str) -> Optional[ServerSpec]:
        if name in self._specs:
            return self._specs[name]
        for spec in self._specs.values():
            if name in spec.aliases:
                return spec
        return None

    def display_name(self, name: str) -> str:
        spec = self.get_spec(name)
        return spec.display_name if spec else name

    def names(self) -> List[str]:
        return list(self._specs.keys())

    def all_clients(self) -> Iterator[Tuple[str, MediaServerClient]]:
        """Yield (name, client) for every successfully-initialized
        server. Used by cross-server operations."""
        for name, instance in self._instances.items():
            if instance is not None:
                yield name, instance


def build_default_registry() -> MediaServerRegistry:
    """Construct the registry with SoulSync's four built-in media
    servers. Called once during MediaServerEngine construction.

    Adding a server (e.g. Subsonic, Emby) = one ``register`` call
    here + the new client class. No dispatch-site changes required.
    """
    registry = MediaServerRegistry()

    registry.register(ServerSpec(name='plex',      factory=PlexClient,      display_name='Plex'))
    registry.register(ServerSpec(name='jellyfin',  factory=JellyfinClient,  display_name='Jellyfin'))
    registry.register(ServerSpec(name='navidrome', factory=NavidromeClient, display_name='Navidrome'))
    registry.register(ServerSpec(name='soulsync',  factory=SoulSyncClient,  display_name='SoulSync Library'))

    return registry
