import toml
import yaml
import logging
import os

from os.path import join, isdir, isfile
from typing import Optional, Any, MutableMapping
from argparse import ArgumentParser
from ..util import parse_addr


def _get_config_path(name: str) -> str:
    formats = ['toml', 'yaml']

    dirname = join(os.environ['HOME'], '.config', name)

    if isdir(dirname):
        for fmt in formats:
            filename = join(dirname, f'config.{fmt}')
            if isfile(filename):
                return filename

        raise IOError(f'config not found in {dirname}')

    else:
        filenames = [join(os.environ['HOME'], '.config', f'{name}.{format}') for format in formats]
        for file in filenames:
            if isfile(file):
                return file

    raise IOError(f'config not found')


class ConfigStore:
    data: MutableMapping[str, Any]
    app_name: Optional[str]

    def __init__(self):
        self.data = {}
        self.app_name = None

    def load(self, name: Optional[str] = None,
             use_cli=True,
             parser: ArgumentParser = None):
        self.app_name = name

        if (name is None) and (not use_cli):
            raise RuntimeError('either config name must be none or use_cli must be True')

        log_default_fmt = False
        log_file = None
        log_verbose = False
        no_config = name is False

        path = None
        if use_cli:
            if parser is None:
                parser = ArgumentParser()
            if not no_config:
                parser.add_argument('-c', '--config', type=str, required=name is None,
                                    help='Path to the config in TOML or YAML format')
            parser.add_argument('-V', '--verbose', action='store_true')
            parser.add_argument('--log-file', type=str)
            parser.add_argument('--log-default-fmt', action='store_true')
            args = parser.parse_args()

            if not no_config and args.config:
                path = args.config

            if args.verbose:
                log_verbose = True
            if args.log_file:
                log_file = args.log_file
            if args.log_default_fmt:
                log_default_fmt = args.log_default_fmt

        if not no_config and path is None:
            path = _get_config_path(name)

        if no_config:
            self.data = {}
        else:
            if path.endswith('.toml'):
                self.data = toml.load(path)
            elif path.endswith('.yaml'):
                with open(path, 'r') as fd:
                    self.data = yaml.safe_load(fd)

        if 'logging' in self:
            if not log_file and 'file' in self['logging']:
                log_file = self['logging']['file']
            if log_default_fmt and 'default_fmt' in self['logging']:
                log_default_fmt = self['logging']['default_fmt']

        setup_logging(log_verbose, log_file, log_default_fmt)

        if use_cli:
            return args

    def __getitem__(self, key):
        return self.data[key]

    def __setitem__(self, key, value):
        raise NotImplementedError('overwriting config values is prohibited')

    def __contains__(self, key):
        return key in self.data

    def get(self, key: str, default=None):
        cur = self.data
        pts = key.split('.')
        for i in range(len(pts)):
            k = pts[i]
            if i < len(pts)-1:
                if k not in cur:
                    raise KeyError(f'key {k} not found')
            else:
                return cur[k] if k in cur else default
            cur = self.data[k]
        raise KeyError(f'option {key} not found')

    def get_addr(self, key: str):
        return parse_addr(self.get(key))

    def items(self):
        return self.data.items()


config = ConfigStore()


def is_development_mode() -> bool:
    if 'HK_MODE' in os.environ and os.environ['HK_MODE'] == 'dev':
        return True

    return ('logging' in config) and ('verbose' in config['logging']) and (config['logging']['verbose'] is True)


def setup_logging(verbose=False, log_file=None, default_fmt=False):
    logging_level = logging.INFO
    if is_development_mode() or verbose:
        logging_level = logging.DEBUG
        _add_logging_level('TRACE', logging.DEBUG-5)

    log_config = {'level': logging_level}
    if not default_fmt:
        log_config['format'] = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'

    if log_file is not None:
        log_config['filename'] = log_file
        log_config['encoding'] = 'utf-8'

    logging.basicConfig(**log_config)


# https://stackoverflow.com/questions/2183233/how-to-add-a-custom-loglevel-to-pythons-logging-facility/35804945#35804945
def _add_logging_level(levelName, levelNum, methodName=None):
    """
    Comprehensively adds a new logging level to the `logging` module and the
    currently configured logging class.

    `levelName` becomes an attribute of the `logging` module with the value
    `levelNum`. `methodName` becomes a convenience method for both `logging`
    itself and the class returned by `logging.getLoggerClass()` (usually just
    `logging.Logger`). If `methodName` is not specified, `levelName.lower()` is
    used.

    To avoid accidental clobberings of existing attributes, this method will
    raise an `AttributeError` if the level name is already an attribute of the
    `logging` module or if the method name is already present

    Example
    -------
    >>> addLoggingLevel('TRACE', logging.DEBUG - 5)
    >>> logging.getLogger(__name__).setLevel("TRACE")
    >>> logging.getLogger(__name__).trace('that worked')
    >>> logging.trace('so did this')
    >>> logging.TRACE
    5

    """
    if not methodName:
        methodName = levelName.lower()

    if hasattr(logging, levelName):
        raise AttributeError('{} already defined in logging module'.format(levelName))
    if hasattr(logging, methodName):
        raise AttributeError('{} already defined in logging module'.format(methodName))
    if hasattr(logging.getLoggerClass(), methodName):
        raise AttributeError('{} already defined in logger class'.format(methodName))

    # This method was inspired by the answers to Stack Overflow post
    # http://stackoverflow.com/q/2183233/2988730, especially
    # http://stackoverflow.com/a/13638084/2988730
    def logForLevel(self, message, *args, **kwargs):
        if self.isEnabledFor(levelNum):
            self._log(levelNum, message, args, **kwargs)
    def logToRoot(message, *args, **kwargs):
        logging.log(levelNum, message, *args, **kwargs)

    logging.addLevelName(levelNum, levelName)
    setattr(logging, levelName, levelNum)
    setattr(logging.getLoggerClass(), methodName, logForLevel)
    setattr(logging, methodName, logToRoot)