"""Synchronize information from music player libraries"""

from __future__ import annotations

from abc import ABCMeta, abstractmethod
from importlib import import_module
from typing import TYPE_CHECKING, ClassVar

from confuse import ConfigValueError

from beets import ui
from beets.plugins import BeetsPlugin

if TYPE_CHECKING:
    from beets.dbcore import types

METASYNC_MODULE = "beetsplug.metasync"

# Dictionary to map the MODULE and the CLASS NAME of meta sources
SOURCES = {"amarok": "Amarok", "itunes": "Itunes"}


class MetaSource(metaclass=ABCMeta):
    item_types: ClassVar[dict[str, types.Type]]

    def __init__(self, config, log):
        self.config = config
        self._log = log

    @abstractmethod
    def sync_from_source(self, item):
        pass


def load_meta_sources():
    """Returns a dictionary of all the MetaSources
    E.g., {'itunes': Itunes} with isinstance(Itunes, MetaSource) true
    """
    meta_sources = {}

    for module_path, class_name in SOURCES.items():
        module = import_module(f"{METASYNC_MODULE}.{module_path}")
        meta_sources[class_name.lower()] = getattr(module, class_name)

    return meta_sources


META_SOURCES = load_meta_sources()


def load_item_types():
    """Returns a dictionary containing the item_types of all the MetaSources"""
    item_types = {}
    for meta_source in META_SOURCES.values():
        item_types.update(meta_source.item_types)
    return item_types


class MetaSyncPlugin(BeetsPlugin):
    item_types = load_item_types()

    def __init__(self):
        super().__init__()

    def commands(self):
        cmd = ui.Subcommand(
            "metasync", help="update metadata from music player libraries"
        )
        cmd.parser.add_option(
            "-p",
            "--pretend",
            action="store_true",
            help="show all changes but do nothing",
        )
        cmd.parser.add_option(
            "-s",
            "--source",
            default=[],
            action="append",
            dest="sources",
            help="comma-separated list of sources to sync",
        )
        cmd.parser.add_format_option()
        cmd.func = self.func
        return [cmd]

    def func(self, lib, opts, args):
        """Command handler for the metasync function."""
        pretend = opts.pretend

        sources = []
        for source in opts.sources:
            sources.extend(source.split(","))

        sources = sources or self.config["source"].as_str_seq()

        meta_source_instances = {}
        items = lib.items(args)

        # Avoid needlessly instantiating meta sources (can be expensive)
        if not items:
            self._log.info("No items found matching query")
            return

        # Instantiate the meta sources
        for player in sources:
            try:
                cls = META_SOURCES[player]
            except KeyError:
                self._log.error("Unknown metadata source '{}'", player)

            try:
                meta_source_instances[player] = cls(self.config, self._log)
            except (ImportError, ConfigValueError) as e:
                self._log.error(
                    "Failed to instantiate metadata source {!r}: {}", player, e
                )

        # Avoid needlessly iterating over items
        if not meta_source_instances:
            self._log.error("No valid metadata sources found")
            return

        # Sync the items with all of the meta sources
        for item in items:
            for meta_source in meta_source_instances.values():
                meta_source.sync_from_source(item)

            changed = ui.show_model_changes(item)

            if changed and not pretend:
                item.store()
