import logging
import threading
from typing import Optional, TypeVar, Type
from typing import Any

import msgspec
from api.v1.schemas.settings import (
    UserPreferences,
    LidarrSettings,
    LidarrConnectionSettings,
    JellyfinConnectionSettings,
    ListenBrainzConnectionSettings,
    YouTubeConnectionSettings,
    HomeSettings,
    LocalFilesConnectionSettings,
    LastFmConnectionSettings,
    ScrobbleSettings,
    PrimaryMusicSourceSettings,
    LASTFM_SECRET_MASK,
    NavidromeConnectionSettings,
    NAVIDROME_PASSWORD_MASK,
    PlexConnectionSettings,
    PLEX_TOKEN_MASK,
)
from api.v1.schemas.profile import ProfileSettings
from api.v1.schemas.advanced_settings import AdvancedSettings
from core.config import Settings
from core.exceptions import ConfigurationError
from infrastructure.file_utils import atomic_write_json, read_json
from infrastructure.serialization import to_jsonable

logger = logging.getLogger(__name__)

T = TypeVar('T', bound=msgspec.Struct)


class PreferencesService:
    def __init__(self, settings: Settings):
        self._settings = settings
        self._config_path = settings.config_file_path
        self._config_cache: Optional[dict] = None
        self._cache_lock = threading.Lock()

    def _load_config(self) -> dict:
        with self._cache_lock:
            if self._config_cache is not None:
                return self._config_cache

            if not self._config_path.exists():
                self._config_cache = {}
                return self._config_cache

            try:
                loaded = read_json(self._config_path, default={})
                self._config_cache = loaded if isinstance(loaded, dict) else {}
            except Exception as e:  # noqa: BLE001
                logger.error(f"Failed to load config: {e}")
                self._config_cache = {}

            return self._config_cache

    def _save_config(self, config: dict) -> None:
        with self._cache_lock:
            self._config_path.parent.mkdir(parents=True, exist_ok=True)
            atomic_write_json(self._config_path, config)
            self._config_cache = config

    def _get_section(self, key: str, model: Type[T], default_factory: Optional[callable] = None) -> T:
        config = self._load_config()
        data = config.get(key, {})
        try:
            if not (isinstance(model, type) and issubclass(model, msgspec.Struct)):
                raise TypeError(f"Preferences section model must be msgspec.Struct, got {model!r}")

            if data:
                return msgspec.convert(data, type=model)
            return default_factory() if default_factory else model()
        except Exception as e:  # noqa: BLE001
            logger.error(f"Failed to parse {key}: {e}")
            return default_factory() if default_factory else model()

    def _save_section(self, key: str, value: Any) -> None:
        config = self._load_config().copy()
        config[key] = to_jsonable(value)
        self._save_config(config)

    def get_preferences(self) -> UserPreferences:
        return self._get_section("user_preferences", UserPreferences)

    def save_preferences(self, preferences: UserPreferences) -> None:
        try:
            self._save_section("user_preferences", preferences)
        except Exception as e:  # noqa: BLE001
            logger.error(f"Failed to save preferences: {e}")
            raise ConfigurationError(f"Failed to save preferences: {e}")

    def get_lidarr_settings(self) -> LidarrSettings:
        return self._get_section("lidarr_settings", LidarrSettings)

    def save_lidarr_settings(self, lidarr_settings: LidarrSettings) -> None:
        try:
            self._save_section("lidarr_settings", lidarr_settings)
        except Exception as e:  # noqa: BLE001
            logger.error(f"Failed to save Lidarr settings: {e}")
            raise ConfigurationError(f"Failed to save Lidarr settings: {e}")

    def get_advanced_settings(self) -> AdvancedSettings:
        return self._get_section("advanced_settings", AdvancedSettings)

    def save_advanced_settings(self, advanced_settings: AdvancedSettings) -> None:
        try:
            self._save_section("advanced_settings", advanced_settings)
        except Exception as e:  # noqa: BLE001
            logger.error(f"Failed to save advanced settings: {e}")
            raise ConfigurationError(f"Failed to save advanced settings: {e}")

    def get_lidarr_connection(self) -> LidarrConnectionSettings:
        config = self._load_config()
        return LidarrConnectionSettings(
            lidarr_url=config.get("lidarr_url", self._settings.lidarr_url),
            lidarr_api_key=config.get("lidarr_api_key", self._settings.lidarr_api_key),
            quality_profile_id=config.get("quality_profile_id", self._settings.quality_profile_id),
            metadata_profile_id=config.get("metadata_profile_id", self._settings.metadata_profile_id),
            root_folder_path=config.get("root_folder_path", self._settings.root_folder_path),
        )

    def save_lidarr_connection(self, settings: LidarrConnectionSettings) -> None:
        try:
            config = self._load_config().copy()
            config.update({
                "lidarr_url": settings.lidarr_url,
                "lidarr_api_key": settings.lidarr_api_key,
                "quality_profile_id": settings.quality_profile_id,
                "metadata_profile_id": settings.metadata_profile_id,
                "root_folder_path": settings.root_folder_path,
            })
            self._save_config(config)

            self._settings.lidarr_url = settings.lidarr_url
            self._settings.lidarr_api_key = settings.lidarr_api_key
            self._settings.quality_profile_id = settings.quality_profile_id
            self._settings.metadata_profile_id = settings.metadata_profile_id
            self._settings.root_folder_path = settings.root_folder_path
        except Exception as e:  # noqa: BLE001
            logger.error(f"Failed to save Lidarr connection settings: {e}")
            raise ConfigurationError(f"Failed to save Lidarr connection settings: {e}")

    def get_jellyfin_connection(self) -> JellyfinConnectionSettings:
        config = self._load_config()
        jellyfin_data = config.get("jellyfin_settings", {})
        return JellyfinConnectionSettings(
            jellyfin_url=jellyfin_data.get("jellyfin_url", config.get("jellyfin_url", self._settings.jellyfin_url)),
            api_key=jellyfin_data.get("api_key", ""),
            user_id=jellyfin_data.get("user_id", ""),
            enabled=jellyfin_data.get("enabled", False),
        )

    def save_jellyfin_connection(self, settings: JellyfinConnectionSettings) -> None:
        try:
            config = self._load_config().copy()
            config["jellyfin_url"] = settings.jellyfin_url
            config["jellyfin_settings"] = {
                "jellyfin_url": settings.jellyfin_url,
                "api_key": settings.api_key,
                "user_id": settings.user_id,
                "enabled": settings.enabled,
            }
            self._save_config(config)

            self._settings.jellyfin_url = settings.jellyfin_url
        except Exception as e:  # noqa: BLE001
            logger.error(f"Failed to save Jellyfin connection settings: {e}")
            raise ConfigurationError(f"Failed to save Jellyfin connection settings: {e}")

    def get_navidrome_connection(self) -> NavidromeConnectionSettings:
        config = self._load_config()
        nd_data = config.get("navidrome_settings", {})
        password = nd_data.get("password", "")
        return NavidromeConnectionSettings(
            navidrome_url=nd_data.get("navidrome_url", ""),
            username=nd_data.get("username", ""),
            password=NAVIDROME_PASSWORD_MASK if password else "",
            enabled=nd_data.get("enabled", False),
        )

    def get_navidrome_connection_raw(self) -> NavidromeConnectionSettings:
        config = self._load_config()
        nd_data = config.get("navidrome_settings", {})
        return NavidromeConnectionSettings(
            navidrome_url=nd_data.get("navidrome_url", ""),
            username=nd_data.get("username", ""),
            password=nd_data.get("password", ""),
            enabled=nd_data.get("enabled", False),
        )

    def save_navidrome_connection(self, settings: NavidromeConnectionSettings) -> None:
        try:
            config = self._load_config().copy()
            current_data = config.get("navidrome_settings", {})

            password = settings.password
            if password == NAVIDROME_PASSWORD_MASK:
                password = current_data.get("password", "")

            config["navidrome_settings"] = {
                "navidrome_url": settings.navidrome_url,
                "username": settings.username,
                "password": password,
                "enabled": settings.enabled,
            }
            self._save_config(config)
        except Exception as e:  # noqa: BLE001
            logger.error("Failed to save Navidrome connection settings: %s", e)
            raise ConfigurationError(f"Failed to save Navidrome connection settings: {e}")

    def get_plex_connection(self) -> PlexConnectionSettings:
        config = self._load_config()
        plex_data = config.get("plex_settings", {})
        settings = PlexConnectionSettings(
            plex_url=plex_data.get("plex_url", ""),
            plex_token=plex_data.get("plex_token", ""),
            enabled=plex_data.get("enabled", False),
            music_library_ids=plex_data.get("music_library_ids", []),
            scrobble_to_plex=plex_data.get("scrobble_to_plex", True),
        )
        if settings.plex_token:
            settings.plex_token = PLEX_TOKEN_MASK
        return settings

    def get_plex_connection_raw(self) -> PlexConnectionSettings:
        config = self._load_config()
        plex_data = config.get("plex_settings", {})
        return PlexConnectionSettings(
            plex_url=plex_data.get("plex_url", ""),
            plex_token=plex_data.get("plex_token", ""),
            enabled=plex_data.get("enabled", False),
            music_library_ids=plex_data.get("music_library_ids", []),
            scrobble_to_plex=plex_data.get("scrobble_to_plex", True),
        )

    def save_plex_connection(self, settings: PlexConnectionSettings) -> None:
        try:
            config = self._load_config().copy()
            current_data = config.get("plex_settings", {})

            token = settings.plex_token
            if token == PLEX_TOKEN_MASK:
                token = current_data.get("plex_token", "")

            config["plex_settings"] = {
                "plex_url": settings.plex_url,
                "plex_token": token,
                "enabled": settings.enabled,
                "music_library_ids": settings.music_library_ids,
                "scrobble_to_plex": settings.scrobble_to_plex,
            }
            self._save_config(config)
            logger.info("Saved Plex connection settings to %s", self._config_path)
        except Exception as e:  # noqa: BLE001
            logger.error("Failed to save Plex connection settings: %s", e)
            raise ConfigurationError(f"Failed to save Plex connection settings: {e}")

    def get_listenbrainz_connection(self) -> ListenBrainzConnectionSettings:
        config = self._load_config()
        lb_data = config.get("listenbrainz_settings", {})
        return ListenBrainzConnectionSettings(
            username=lb_data.get("username", ""),
            user_token=lb_data.get("user_token", ""),
            enabled=lb_data.get("enabled", False),
        )

    def save_listenbrainz_connection(self, settings: ListenBrainzConnectionSettings) -> None:
        try:
            config = self._load_config().copy()
            config["listenbrainz_settings"] = {
                "username": settings.username,
                "user_token": settings.user_token,
                "enabled": settings.enabled,
            }
            self._save_config(config)
        except Exception as e:  # noqa: BLE001
            logger.error(f"Failed to save ListenBrainz connection settings: {e}")
            raise ConfigurationError(f"Failed to save ListenBrainz connection settings: {e}")

    def get_youtube_connection(self) -> YouTubeConnectionSettings:
        config = self._load_config()
        yt_data = config.get("youtube_settings", {})
        api_key = str(yt_data.get("api_key") or "")
        enabled = yt_data.get("enabled", False)
        # Auto-migrate: existing setups with enabled+api_key get api_enabled=True
        if "api_enabled" not in yt_data and enabled and api_key.strip():
            api_enabled = True
        else:
            api_enabled = yt_data.get("api_enabled", False)
        return YouTubeConnectionSettings(
            api_key=api_key,
            enabled=enabled,
            api_enabled=api_enabled,
            daily_quota_limit=yt_data.get("daily_quota_limit", 80),
        )

    def save_youtube_connection(self, settings: YouTubeConnectionSettings) -> None:
        try:
            config = self._load_config().copy()
            config["youtube_settings"] = {
                "api_key": settings.api_key.strip(),
                "enabled": settings.enabled,
                "api_enabled": settings.api_enabled,
                "daily_quota_limit": settings.daily_quota_limit,
            }
            self._save_config(config)
        except Exception as e:  # noqa: BLE001
            logger.error(f"Failed to save YouTube connection settings: {e}")
            raise ConfigurationError(f"Failed to save YouTube connection settings: {e}")

    def get_home_settings(self) -> HomeSettings:
        return self._get_section("home_settings", HomeSettings)

    def save_home_settings(self, settings: HomeSettings) -> None:
        try:
            self._save_section("home_settings", settings)
        except Exception as e:  # noqa: BLE001
            logger.error(f"Failed to save home settings: {e}")
            raise ConfigurationError(f"Failed to save home settings: {e}")

    def get_local_files_connection(self) -> LocalFilesConnectionSettings:
        return self._get_section("local_files_settings", LocalFilesConnectionSettings)

    def save_local_files_connection(self, settings: LocalFilesConnectionSettings) -> None:
        try:
            self._save_section("local_files_settings", settings)
        except Exception as e:  # noqa: BLE001
            logger.error("Failed to save local files settings: %s", e)
            raise ConfigurationError(f"Failed to save local files settings: {e}")

    def get_lastfm_connection(self) -> LastFmConnectionSettings:
        return self._get_section("lastfm_settings", LastFmConnectionSettings)

    def save_lastfm_connection(self, settings: LastFmConnectionSettings) -> None:
        try:
            current = self.get_lastfm_connection()

            api_key = settings.api_key.strip()
            shared_secret = settings.shared_secret
            if shared_secret.startswith(LASTFM_SECRET_MASK):
                shared_secret = current.shared_secret
            else:
                shared_secret = shared_secret.strip()

            session_key = settings.session_key
            if session_key.startswith(LASTFM_SECRET_MASK):
                session_key = current.session_key
            else:
                session_key = session_key.strip()

            username = settings.username.strip()
            enabled = settings.enabled
            if not api_key or not shared_secret:
                enabled = False
                session_key = ""
                username = ""

            resolved = LastFmConnectionSettings(
                api_key=api_key,
                shared_secret=shared_secret,
                session_key=session_key,
                username=username,
                enabled=enabled,
            )
            self._save_section("lastfm_settings", resolved)
        except Exception as e:  # noqa: BLE001
            logger.error("Failed to save Last.fm connection settings: %s", e)
            raise ConfigurationError(f"Failed to save Last.fm connection settings: {e}")

    def is_lastfm_enabled(self) -> bool:
        settings = self.get_lastfm_connection()
        return settings.enabled and bool(settings.api_key) and bool(settings.shared_secret)

    def get_scrobble_settings(self) -> ScrobbleSettings:
        return self._get_section("scrobble_settings", ScrobbleSettings)

    def save_scrobble_settings(self, settings: ScrobbleSettings) -> None:
        try:
            self._save_section("scrobble_settings", settings)
        except Exception as e:  # noqa: BLE001
            logger.error("Failed to save scrobble settings: %s", e)
            raise ConfigurationError(f"Failed to save scrobble settings: {e}")

    def get_primary_music_source(self) -> PrimaryMusicSourceSettings:
        return self._get_section("primary_music_source", PrimaryMusicSourceSettings)

    def save_primary_music_source(self, settings: PrimaryMusicSourceSettings) -> None:
        try:
            self._save_section("primary_music_source", settings)
        except Exception as e:  # noqa: BLE001
            logger.error("Failed to save primary music source: %s", e)
            raise ConfigurationError(f"Failed to save primary music source: {e}")

    def get_profile_settings(self) -> ProfileSettings:
        return self._get_section("profile_settings", ProfileSettings)

    def save_profile_settings(self, settings: ProfileSettings) -> None:
        try:
            self._save_section("profile_settings", settings)
        except Exception as e:  # noqa: BLE001
            logger.error("Failed to save profile settings: %s", e)
            raise ConfigurationError(f"Failed to save profile settings: {e}")

    def get_setting(self, key: str) -> Any:
        config = self._load_config()
        internal = config.get("_internal", {})
        return internal.get(key)

    def save_setting(self, key: str, value: Any) -> None:
        config = self._load_config().copy()
        internal = config.get("_internal", {}).copy()
        if value is None:
            internal.pop(key, None)
        else:
            internal[key] = value
        config["_internal"] = internal
        self._save_config(config)

    def get_or_create_setting(self, key: str, factory: Any) -> Any:
        """Atomically get or create an internal setting under the cache lock."""
        with self._cache_lock:
            config = self._load_config()
            internal = config.get("_internal", {})
            value = internal.get(key)
            if value:
                return value
            value = factory() if callable(factory) else factory
            config = config.copy()
            internal = internal.copy()
            internal[key] = value
            config["_internal"] = internal
            self._save_config(config)
            return value
