"""Plugin registry — single source of truth for which download
sources exist, what their canonical names are, and which client
class implements each.

Replaces the orchestrator's hardcoded ``[self.soulseek,
self.youtube, self.tidal, ...]`` lists and ``source_map`` dicts
that historically had to be touched in 6+ places to add a source.
With the registry:

- One ``register()`` call adds a source to every dispatch path.
- Iteration helpers replace hand-maintained lists.
- The orchestrator stays oblivious to source-specific quirks.
- Adding Usenet (planned) becomes a one-line registry entry plus
  the new client class — no orchestrator changes.

This is the foundation step. Subsequent commits move shared logic
(thread workers, search query normalization, post-processing
context building) out of the orchestrator and the per-source
clients into helpers the registry exposes.
"""

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.download_plugins.base import DownloadSourcePlugin

# Eager client imports — keep the import-order behavior the orchestrator
# had before this refactor. Some integration tests inject mock modules
# into ``sys.modules`` at collection time (see
# ``tests/test_tidal_search_shortening.py``); making the registry's
# factories lazy-import would cause tidalapi-dependent code to bind to
# whichever ``tidalapi`` object happens to be in ``sys.modules`` at the
# moment ``DownloadOrchestrator()`` is constructed — which is later
# than the legacy module-top imports here. Importing everything at
# registry-load time pins the bindings the same way the legacy
# orchestrator did.
from core.amazon_download_client import AmazonDownloadClient
from core.deezer_download_client import DeezerDownloadClient
from core.download_plugins.torrent import TorrentDownloadPlugin
from core.download_plugins.usenet import UsenetDownloadPlugin
from core.hifi_client import HiFiClient
from core.lidarr_download_client import LidarrDownloadClient
from core.qobuz_client import QobuzClient
from core.soulseek_client import SoulseekClient
from core.soundcloud_client import SoundcloudClient
from core.tidal_download_client import TidalDownloadClient
from core.youtube_client import YouTubeClient

logger = get_logger("download_plugins.registry")


@dataclass(frozen=True)
class PluginSpec:
    """Static descriptor for a download source. The ``factory`` is
    a zero-arg callable that builds the client instance — kept as a
    callable rather than a class so each source can do its own
    setup (e.g. SoulseekClient calls ``_setup_client`` after init,
    Deezer reads ARL from config). ``aliases`` lets the registry
    accept multiple historical names (e.g. ``deezer_dl`` is the
    legacy alias for ``deezer``)."""

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


class DownloadPluginRegistry:
    """Holds the live plugin instances + name → instance lookup.

    Construction is two-phase:
    1. Specs are registered (cheap — just stores callable refs).
    2. ``initialize()`` calls each factory once and stores the
       resulting client. Failures are caught and logged so one
       broken source doesn't take down the orchestrator (mirrors
       the existing ``_safe_init`` behavior).

    Iteration helpers (``all_plugins``, ``configured_plugins``)
    replace the hand-maintained lists scattered across the
    orchestrator's ``get_all_downloads``, ``cancel_all_downloads``,
    etc. so adding a source touches the registry alone.
    """

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

    def register(self, spec: PluginSpec) -> None:
        """Register a plugin spec under its canonical name + each alias.
        Aliases all resolve to the same instance after ``initialize``."""
        if spec.name in self._specs:
            raise ValueError(f"Plugin already registered: {spec.name}")
        self._specs[spec.name] = spec

    def initialize(self) -> None:
        """Build every registered plugin's instance. Failures captured
        in ``init_failures`` and the slot is set to None so the
        orchestrator can skip unavailable sources without crashing."""
        for spec in self._specs.values():
            try:
                instance = spec.factory()
                self._instances[spec.name] = instance
            except Exception as exc:
                logger.error("%s download client failed to initialize: %s", spec.display_name, exc)
                self._init_failures.append(spec.display_name)
                self._instances[spec.name] = None

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

    def get(self, name: str) -> Optional[DownloadSourcePlugin]:
        """Resolve a name (or alias) to its plugin instance, or
        None if the source failed to initialize / isn't registered."""
        if not name:
            return None
        # Direct hit
        if name in self._instances:
            return self._instances[name]
        # Alias lookup
        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[PluginSpec]:
        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]:
        """Canonical names of every registered source (regardless of
        whether it initialized successfully)."""
        return list(self._specs.keys())

    def all_plugins(self) -> Iterator[Tuple[str, DownloadSourcePlugin]]:
        """Yield (name, plugin) for every successfully-initialized
        plugin. Replaces the orchestrator's hand-maintained client
        lists in get_all_downloads / cancel_all_downloads / etc."""
        for name, instance in self._instances.items():
            if instance is not None:
                yield name, instance

    def configured_plugins(self) -> Iterator[Tuple[str, DownloadSourcePlugin]]:
        """Yield (name, plugin) for every initialized AND configured
        plugin. Useful for hybrid mode and any operation that should
        skip sources the user hasn't set up."""
        for name, instance in self.all_plugins():
            try:
                if instance.is_configured():
                    yield name, instance
            except Exception:
                continue


def build_default_registry() -> DownloadPluginRegistry:
    """Construct the registry with SoulSync's eight built-in download
    sources. Called once during orchestrator init.

    Adding a new source (Usenet, etc.) means adding one ``register``
    call here — no orchestrator changes required.

    The factory itself is just the class constructor — clients are
    imported eagerly at the top of this module so they bind to the
    real third-party libs (tidalapi, etc.) at import time, not at
    factory-call time. See the import-block comment above for why.
    """
    registry = DownloadPluginRegistry()

    registry.register(PluginSpec(name='amazon',    factory=AmazonDownloadClient,   display_name='Amazon Music'))
    registry.register(PluginSpec(name='soulseek',  factory=SoulseekClient,         display_name='Soulseek'))
    registry.register(PluginSpec(name='youtube',   factory=YouTubeClient,          display_name='YouTube'))
    registry.register(PluginSpec(name='tidal',     factory=TidalDownloadClient,    display_name='Tidal'))
    registry.register(PluginSpec(name='qobuz',     factory=QobuzClient,            display_name='Qobuz'))
    registry.register(PluginSpec(name='hifi',      factory=HiFiClient,             display_name='HiFi'))
    # 'deezer_dl' is the legacy name used in config + per-source dispatch
    # strings (e.g. orchestrator's ``source_map``). Canonical name is
    # ``deezer`` so future-facing code reads naturally.
    registry.register(PluginSpec(name='deezer',    factory=DeezerDownloadClient,   display_name='Deezer',
                                 aliases=('deezer_dl',)))
    registry.register(PluginSpec(name='lidarr',    factory=LidarrDownloadClient,   display_name='Lidarr'))
    registry.register(PluginSpec(name='soundcloud',factory=SoundcloudClient,       display_name='SoundCloud'))
    registry.register(PluginSpec(name='torrent',   factory=TorrentDownloadPlugin,  display_name='Torrent (Prowlarr)'))
    registry.register(PluginSpec(name='usenet',    factory=UsenetDownloadPlugin,   display_name='Usenet (Prowlarr)'))

    return registry
