# -*- coding: UTF-8 -*-

from weakref import WeakValueDictionary
from ast import literal_eval
from configparser import ExtendedInterpolation, ConfigParser
from decimal import Decimal
from os.path import isfile, exists, realpath
from sys import argv
from threading import RLock
from typing import Iterator, Sequence, Union, List, Tuple, Dict

from .constants import Key, Value, ROOT, CONFIG
from .exceptions import ArgParseError
from .utils import ensure_folder, folder, file


class ArgsParser(object):

    @staticmethod
    def _update_params(params: dict, section: str, option: str, value: str):

        if section not in params:
            params.update({section: {option: value}})
        else:
            params.get(section).update({option: value})

    @classmethod
    def parse(cls, args: Iterator[str]) -> dict:
        temp = dict()

        for arg in args:
            if arg.startswith("--") is True:
                stripped = arg.strip("-")
                try:
                    section, option = stripped.split("-")
                except ValueError:
                    raise ArgParseError(f"Inconsistency in cmd-line parameters '{arg}'!")
                else:
                    try:
                        value = next(args)
                    except StopIteration:
                        raise ArgParseError(f"Missing value for parameter '{arg}'")
                    else:
                        if value.startswith("--") is False:
                            cls._update_params(temp, section.upper(), option, value)
                        else:
                            raise ArgParseError(f"Incorrect value '{value}' for parameter '{arg}'!")
            else:
                raise ArgParseError(f"Inconsistency in cmd-line parameters '{arg}'!")

        return temp


class CfgParser(ConfigParser, ArgsParser):
    """Configuration handle."""

    _DEFAULT_CONVERTERS: dict = {
        "decimal": Decimal,
        "list": literal_eval,
        "tuple": literal_eval,
        "set": literal_eval,
        "dict": literal_eval,
        "path": realpath,
        "folder": folder,
        "file": file,
    }

    _DEFAULTS: dict = {
        "directory": ROOT,
    }

    __locks__ = WeakValueDictionary()

    @staticmethod
    def _as_dict(mapping: Union[Dict, List[Tuple[Key, Value]]] = None, **kwargs) -> dict:
        if isinstance(mapping, list):
            mapping = dict(mapping)

        elif mapping is None:
            mapping = dict()

        if len(kwargs) > 0:
            mapping.update(kwargs)

        return mapping

    @staticmethod
    def _exists(item: str) -> bool:
        return exists(item) and isfile(item)

    def __init__(self, name: str = "cfgpie", **kwargs):

        self._name = name
        self._thread_lock: RLock = self._dispatch(self._name)

        super(CfgParser, self).__init__(**self._default_params(kwargs))

    @property
    def name(self):
        return self._name

    def parse(self, args: Sequence[str] = None):
        """Parse command-line arguments and update the configuration."""
        with self._thread_lock:
            if args is None:
                args = argv[1:]

            if len(args) > 0:
                self.read_dict(
                    dictionary=super(CfgParser, self).parse(iter(args)),
                    source="<cmd-line>"
                )

    def set_defaults(self, mapping: Union[Dict, List[Tuple[Key, Value]]] = None, **kwargs):
        """Update `DEFAULT` section with `mapping` & `kwargs`."""
        with self._thread_lock:
            kwargs: dict = self._as_dict(mapping, **kwargs)

            if len(kwargs) > 0:
                self._read_defaults(kwargs)

    def open(self, file_path: Union[str, List[str]], encoding: str = "UTF-8", fallback: dict = None):
        """
        Read from configuration `file_path` which can also be a list of files paths.
        If `file_path` does not exist and `fallback` is provided
        the latter will be used and a new configuration file will be written.
        """
        with self._thread_lock:
            if isinstance(file_path, str):
                file_path = [file_path]

            if any([self._exists(item) for item in file_path]):
                self.read(file_path, encoding=encoding)

            elif fallback is not None:
                self.read_dict(dictionary=fallback, source="<backup>")
                self.save(CONFIG, encoding)

    def save(self, file_path: str, encoding: str):
        """Save the configuration to `file_path`."""
        with self._thread_lock:
            ensure_folder(file_path)
            with open(file_path, "w", encoding=encoding) as fh:
                self.write(fh)

    def _default_params(self, kwargs: dict) -> dict:
        with self._thread_lock:
            temp: dict = kwargs.copy()
            kwargs.update(
                defaults=temp.pop("defaults", self._DEFAULTS),
                interpolation=temp.pop("interpolation", ExtendedInterpolation()),
                converters=self._get_converters(temp),
            )
            return kwargs

    def _get_converters(self, kwargs: dict) -> dict:
        if "converters" in kwargs:
            return self._merge_converters(**kwargs.pop("converters"))
        return self._DEFAULT_CONVERTERS

    def _merge_converters(self, **kwargs) -> dict:
        converters: dict = self._DEFAULT_CONVERTERS.copy()
        converters.update(**kwargs)
        return converters

    def _dispatch(self, name: str) -> RLock:
        if name not in self.__locks__:
            instance = RLock()
            self.__locks__.update({name: instance})
        return self.__locks__.get(name)
