from typing import Any, Tuple, Union

import json
import os
import uuid
import tomlkit

from pathlib import Path
from pydantic import BaseSettings, SecretStr, root_validator
from pydantic.env_settings import SettingsSourceCallable

from ..utils import FlexBytes
from .functions import package_settings, home_settings, current_settings
from .telemetry import TelemetrySettings
from .server import ServerSettings
from .client import ClientSettings
from ..db import DatabaseSettings
from ..utils import crypto


class Settings(BaseSettings):
    """
    Top level configuration settings
    """

    grand_central: str
    """
    Server and port where grand central is located
    """

    grand_central_pk: FlexBytes
    """
    Public key used to verify signatures generated by GC 
    """

    client: ClientSettings = ClientSettings()
    """
    Where is the server?
    """

    telemetry: TelemetrySettings = TelemetrySettings(enabled=True)
    """
    Telemetry Settings 
    """

    server: ServerSettings = ServerSettings()
    """
    Server settings
    """

    database: DatabaseSettings = DatabaseSettings()
    """
    Database settings 
    """

    @root_validator
    def check_rt_settings(cls, values):
        """
        This root validator checks that values with no sensible default
        are persisted in user's home directory.
        """
        svr_entry = values.get('server')
        tel_entry = values.get('telemetry')
        if (svr_entry.secret is not None) and (svr_entry.salt is not None) and (tel_entry.id is not None):
            # Non need to do anything
            return values

        # get the current or generate a new value
        svr_entry.salt = svr_entry.salt or FlexBytes(crypto.generate_salt())
        svr_entry.secret = svr_entry.secret or SecretStr(crypto.generate_random_password().decode('ascii'))
        tel_entry.id = tel_entry.id or uuid.uuid4()

        # ensure ~/.shapelets exists
        home_dir = os.path.expanduser('~/.shapelets')
        os.makedirs(home_dir, exist_ok=True)
        settings_file = os.path.join(home_dir, 'settings.toml')

        # load existing file or create a new configuration document
        if os.path.exists(settings_file):
            with open(settings_file, "rt", encoding="utf-8") as handle:
                doc = tomlkit.load(handle)
        else:
            doc = tomlkit.document()

        # ensure server section
        if 'server' not in doc:
            server_doc = tomlkit.table()
            server_doc.add(tomlkit.comment("Settings for local server instance"))
            doc['server'] = server_doc
        else:
            server_doc = doc['server']

        # ensure telementry section
        if 'telemetry' not in doc:
            telemetry_doc = tomlkit.table()
            doc['telemetry'] = telemetry_doc
        else:
            telemetry_doc = doc['telemetry']

        # save the results
        server_doc['salt'] = str(svr_entry.salt)  # saved as encoded string
        server_doc['secret'] = svr_entry.secret.get_secret_value()  # saved plain
        telemetry_doc['id'] = str(tel_entry.id)  # uuid to str

        # save the document; tomlkit should retain the all settings
        # and comments already present in the document.
        with open(settings_file, "wt", encoding="utf-8") as handle:
            tomlkit.dump(doc, handle)

        # return
        return values

    def save(self,
             loc: Union[Path, str] = '~/.shapelets/settings.toml',
             exclude_unset: bool = True,
             exclude_defaults: bool = True):

        def remove_empty_keys(data: Any):
            ks = [k for k in data if isinstance(data[k], dict)]
            for k in ks:
                v = data[k]
                if len(v) == 0:
                    del data[k]
                else:
                    remove_empty_keys(v)
            return data

        file = Path(os.path.expandvars(os.path.expanduser(loc)))
        if not file.exists():
            file.parent.mkdir(exist_ok=True)

        with open(file, "wt", encoding="utf-8") as handle:
            json_str = self.json(skip_defaults=True,
                                 exclude_unset=exclude_unset,
                                 exclude_defaults=exclude_defaults,
                                 exclude_none=True,
                                 encoder=lambda o: o.get_secret_value() if isinstance(o, SecretStr) else str(o))
            data = remove_empty_keys(json.loads(json_str))
            tomlkit.dump(data, handle)

    class Config:
        env_prefix = "shapelets_"
        """
        Prefix for all environment variables that can override configuration values
        """
        case_sensitive = False
        """
        Environment variables are case insensitive, which makes Windows and non 
        Windows systems to behave similarly.
        """
        env_nested_delimiter = '__'
        """
        Inner field separator for environment variables.
        """

        json_encoders = {
            FlexBytes: lambda v: str(v)
        }

        @classmethod
        def customise_sources(cls,
                              init_settings: SettingsSourceCallable,
                              env_settings: SettingsSourceCallable,
                              file_secret_settings: SettingsSourceCallable) -> Tuple[SettingsSourceCallable, ...]:
            """
            Returns the order of readers for configuration settings, in order of 
            priority, that is, the first readers will override the rest.

            Notes
            -----
            The order of reading is as follows:

            1. Firstly, read factory settings, then, 
            2. Read settings stored in the home directory, then,
            3. Read settings from the current directory, then, 
            4. Read settings from environment variables, and finally,
            5. Apply settings passed directly by code.
            """

            # build the list in the order of reading
            readers = [package_settings(), home_settings(), current_settings(), env_settings, init_settings]
            # remove null readers (as the file may not exist)
            readers = list(filter(None, readers))
            # reverse (so final readers appear first)
            # and convert to tuple.
            return tuple(reversed(readers))
