import hashlib
import logging
import os
import pickle
from dataclasses import asdict, dataclass, field, fields
from enum import Enum
from pathlib import Path
from typing import List, Optional

try:
    import keyring

    has_keyring = True
except ImportError:
    has_keyring = False

import yaml

from sublime.state_manager import ApplicationState
from sublime.ui.state import UIState


class ReplayGainType(Enum):
    NO = 0
    TRACK = 1
    ALBUM = 2

    def as_string(self) -> str:
        return ["no", "track", "album"][self.value]

    @staticmethod
    def from_string(replay_gain_type: str) -> "ReplayGainType":
        return {
            "no": ReplayGainType.NO,
            "disabled": ReplayGainType.NO,
            "track": ReplayGainType.TRACK,
            "album": ReplayGainType.ALBUM,
        }[replay_gain_type.lower()]


@dataclass(unsafe_hash=True)
class ServerConfiguration:
    name: str = "Default"
    server_address: str = "http://yourhost"
    local_network_address: str = ""
    local_network_ssid: str = ""
    username: str = ""
    password: str = ""
    sync_enabled: bool = True
    disable_cert_verify: bool = False
    version: int = 0

    def migrate(self):
        self.version = 0

    def strhash(self) -> str:
        # TODO: needs to change to something better
        """
        Returns the MD5 hash of the server's name, server address, and
        username. This should be used whenever it's necessary to uniquely
        identify the server, rather than using the name (which is not
        necessarily unique).

        >>> sc = ServerConfiguration(
        ...     name='foo',
        ...     server_address='bar',
        ...     username='baz',
        ... )
        >>> sc.strhash()
        '6df23dc03f9b54cc38a0fc1483df6e21'
        """
        server_info = self.name + self.server_address + self.username
        return hashlib.md5(server_info.encode("utf-8")).hexdigest()


@dataclass
class AppConfiguration:
    servers: List[ServerConfiguration] = field(default_factory=list)
    current_server_index: int = -1
    cache_location: str = ""
    max_cache_size_mb: int = -1  # -1 means unlimited
    always_stream: bool = False  # always stream instead of downloading songs
    download_on_stream: bool = True  # also download when streaming a song
    song_play_notification: bool = True
    prefetch_amount: int = 3
    concurrent_download_limit: int = 5
    port_number: int = 8282
    version: int = 3
    serve_over_lan: bool = True
    replay_gain: ReplayGainType = ReplayGainType.NO
    filename: Optional[Path] = None

    @staticmethod
    def load_from_file(filename: Path) -> "AppConfiguration":
        args = {}
        if filename.exists():
            with open(filename, "r") as f:
                field_names = {f.name for f in fields(AppConfiguration)}
                args = yaml.load(f, Loader=yaml.CLoader).items()
                args = dict(filter(lambda kv: kv[0] in field_names, args))

        config = AppConfiguration(**args)
        config.filename = filename

        return config

    def __post_init__(self):
        # Default the cache_location to ~/.local/share/sublime-music
        if not self.cache_location:
            path = Path(os.environ.get("XDG_DATA_HOME") or "~/.local/share")
            path = path.expanduser().joinpath("sublime-music").resolve()
            self.cache_location = path.as_posix()

        # Deserialize the YAML into the ServerConfiguration object.
        if len(self.servers) > 0 and type(self.servers[0]) != ServerConfiguration:
            self.servers = [ServerConfiguration(**sc) for sc in self.servers]

        self._state = None
        self._current_server_hash = None

    def migrate(self):
        for server in self.servers:
            server.migrate()
        self.version = 3
        self.state.migrate()

    @property
    def server(self) -> Optional[ServerConfiguration]:
        if 0 <= self.current_server_index < len(self.servers):
            return self.servers[self.current_server_index]

        return None

    @property
    def state(self) -> UIState:
        server = self.server
        if not server:
            return UIState()

        # If the server has changed, then retrieve the new server's state.
        # TODO: if things are slow, then use a different hash
        if self._current_server_hash != server.strhash():
            self.load_state()

        return self._state

    def load_state(self):
        if not self.server:
            return

        self._current_server_hash = self.server.strhash()
        if self.state_file_location.exists():
            try:
                with open(self.state_file_location, "rb") as f:
                    self._state = UIState(**pickle.load(f))
            except Exception:
                logging.warning(f"Couldn't load state from {self.state_file_location}")
                # Just ignore any errors, it is only UI state.
                self._state = UIState()

        # Do the import in the function to avoid circular imports.
        from sublime.cache_manager import CacheManager
        from sublime.adapters import AdapterManager

        CacheManager.reset(self)
        AdapterManager.reset(self)

    @property
    def state_file_location(self) -> Path:
        assert self.server is not None
        server_hash = self.server.strhash()

        state_file_location = Path(os.environ.get("XDG_DATA_HOME") or "~/.local/share")
        return state_file_location.expanduser().joinpath(
            "sublime-music", server_hash, "state.pickle"
        )

    def save(self):
        # Save the config as YAML.
        self.filename.parent.mkdir(parents=True, exist_ok=True)
        with open(self.filename, "w+") as f:
            f.write(yaml.dump(asdict(self)))

        # Save the state for the current server.
        self.state_file_location.parent.mkdir(parents=True, exist_ok=True)
        with open(self.state_file_location, "wb+") as f:
            pickle.dump(asdict(self.state), f)
