# -*- coding: utf-8 -*-
"""
Module for handling the settings of gitcache.

Copyright:
    2020 by Clemens Rabe <clemens.rabe@clemensrabe.de>

    All rights reserved.

    This file is part of gitcache (https://github.com/seeraven/gitcache)
    and is released under the "BSD 3-Clause License". Please see the ``LICENSE`` file
    that is included as part of this package.
"""


# -----------------------------------------------------------------------------
# Module Import
# -----------------------------------------------------------------------------
import configparser
import logging
import os
import re

import pytimeparse

from .command_execution import getstatusoutput
from .global_settings import GITCACHE_DIR


# -----------------------------------------------------------------------------
# Logger
# -----------------------------------------------------------------------------
LOG = logging.getLogger(__name__)


# -----------------------------------------------------------------------------
# Functions
# -----------------------------------------------------------------------------
def str_to_regex(string):
    """Convert a string to a regex pattern with special treatment of an empty string."""
    if string:
        return string
    return "a^"         # This pattern should never match


def str_to_bool(string):
    """Convert a string to a boolean value."""
    if string.upper() in ["1", "ON", "TRUE", "YES"]:
        return True
    return False


def str_to_seconds(string):
    """Convert a string to a seconds value."""
    seconds = pytimeparse.parse(string)
    if seconds is None:
        try:
            seconds = int(string.strip())
        except ValueError:
            seconds = 0
    return seconds


def has_git_lfs_cmd():
    """Check whether this host has the git-lfs command available.

    Return:
        Returns True if the git-lfs command is available.
    """
    if not hasattr(has_git_lfs_cmd, "has_git_lfs"):
        retval, _ = getstatusoutput('git-lfs version 2>/dev/null')
        has_git_lfs_cmd.has_git_lfs = (retval == 0)
    return has_git_lfs_cmd.has_git_lfs


# -----------------------------------------------------------------------------
# Class Definition
# -----------------------------------------------------------------------------
class ConfigItem:
    """All information about a configuration item.

    Attributes:
        section (str):       The section in the configuration file.
        option (str):        The option name.
        default:             The default value.
        converter:           Converter from string to the return value.
        env (str):           The name of the environment variable or None.
    """

    camel_to_snake = re.compile(r'(?<!^)(?=[A-Z])')

    # pylint: disable=too-many-arguments
    def __init__(self, section, option, default, converter=str_to_seconds, env='auto'):
        """Construct a configuration item.

        Args:
            section (str): The section in the configuration file.
            option (str):  The option name.
            default:       The default value.
            converter:     The converter used to convert the string to the
                           return value.
            env (str):     The name of the environment variable. If set to
                           None, no environment variable will be registered.
                           If set to 'auto', the name of the environment variable
                           is constructed as 'GITCACHE_<SECTION>_<OPTION>'.
        """
        self.section = section
        self.option = option
        self.default = default
        self.converter = converter
        self.env = env
        if self.env == 'auto':
            self.env = f'GITCACHE_{self.section.upper()}_{self._get_snake_upper(self.option)}'

    @staticmethod
    def _get_snake_upper(camel):
        """Convert the given CamelCase string into a SNAKE_CASE string.

        Args:
            camel (str): The CamelCase string.

        Return:
            Returns the SNAKE_CASE string.
        """
        return ConfigItem.camel_to_snake.sub('_', camel).upper()

    def add_to_configparser(self, config):
        """Add the configuration item to the configparser.ConfigParser object.

        Args:
            config (configparser.ConfigParser): The config parser object.
        """
        if self.section not in config.sections():
            config.add_section(self.section)

        config.set(self.section, self.option, str(self.default))

    def add_to_env_keys(self, env_keys):
        """Add the configuration item to the env_keys map.

        Args:
            env_keys (map): The env_keys map to add the environment key.
        """
        if self.env:
            env_section = self.section.upper()
            env_option = self.option.upper()
            if env_section not in env_keys:
                env_keys[env_section] = {}
            env_keys[env_section][env_option] = self.env

    def add_to_converters(self, converters):
        """Add the configuration item to the converters map.

        Args:
            converters (map): The converters map to add the converter function.
        """
        section = self.section.upper()
        option = self.option.upper()
        if section not in converters:
            converters[section] = {}
        converters[section][option] = self.converter


class Config:
    """The configuration of gitcache."""

    def __init__(self):
        """Initialize the configuration.

        Initialize the configuration by setting the default values and
        loading the global configuration file.
        """
        self.items = []
        self.items.append(ConfigItem('System', 'RealGit', '/usr/bin/git',
                                     converter=str,
                                     env='GITCACHE_REAL_GIT'))

        self.items.append(ConfigItem('MirrorHandling', 'UpdateInterval', '0 seconds',
                                     env='GITCACHE_UPDATE_INTERVAL'))
        self.items.append(ConfigItem('MirrorHandling', 'CleanupAfter', '14 days',
                                     env='GITCACHE_CLEANUP_AFTER'))

        self.items.append(ConfigItem('UrlPatterns', 'IncludeRegex', '.*', converter=str_to_regex))
        self.items.append(ConfigItem('UrlPatterns', 'ExcludeRegex', '', converter=str_to_regex))

        self.items.append(ConfigItem('Command', 'WarnIfLockedFor', '10 seconds'))
        self.items.append(ConfigItem('Command', 'CheckInterval', '2 seconds'))
        self.items.append(ConfigItem('Command', 'LockTimeout', '1 hour'))

        self.items.append(ConfigItem('Clone', 'Retries', 3, converter=int))
        self.items.append(ConfigItem('Clone', 'CommandTimeout', '1 hour'))
        self.items.append(ConfigItem('Clone', 'OutputTimeout', '5 minutes'))

        self.items.append(ConfigItem('Update', 'Retries', 3, converter=int))
        self.items.append(ConfigItem('Update', 'CommandTimeout', '1 hour'))
        self.items.append(ConfigItem('Update', 'OutputTimeout', '5 minutes'))

        self.items.append(ConfigItem('GC', 'Retries', 3, converter=int))
        self.items.append(ConfigItem('GC', 'CommandTimeout', '1 hour'))
        self.items.append(ConfigItem('GC', 'OutputTimeout', '5 minutes'))

        self.items.append(ConfigItem('LFS', 'Retries', 3, converter=int))
        self.items.append(ConfigItem('LFS', 'CommandTimeout', '1 hour'))
        self.items.append(ConfigItem('LFS', 'OutputTimeout', '5 minutes'))
        self.items.append(ConfigItem('LFS', 'PerMirrorStorage', True, converter=str_to_bool))

        self.config = configparser.ConfigParser()
        self.env_keys = {}
        self.converters = {}
        for item in self.items:
            item.add_to_configparser(self.config)
            item.add_to_env_keys(self.env_keys)
            item.add_to_converters(self.converters)

        if not self.load(os.path.join(GITCACHE_DIR, 'config')):
            self.save(os.path.join(GITCACHE_DIR, 'config'))

    def get(self, section, option):
        """Get a configuration value.

        Args:
            section (str): The section, e.g., 'Clone'.
            option (str):  The option, e.g., 'Retries'.
        Return:
            Returns the current value of the specified option.
        """
        value = None
        env_key = self.env_keys.get(section.upper(), {}).get(option.upper())
        converter = self.converters.get(section.upper(), {}).get(option.upper())

        if env_key:
            value = os.getenv(env_key, None)
        if value is None:
            value = self.config.get(section, option)

        if converter:
            value = converter(value)

        return value

    def load(self, filename):
        """Load the configuration file.

        Args:
            filename (str): The file to read.
        Return:
            Returns True if the configuration file was found and loaded.
        """
        if os.path.exists(filename):
            LOG.debug("Loading configuration file %s.", filename)
            with open(filename, 'r', encoding='utf-8') as file_handle:
                self.config.read_file(file_handle, source=filename)
                return True

        LOG.debug("Can't load configuration file %s as it does not exist!", filename)
        return False

    def save(self, filename):
        """Save the configuration file.

        Args:
            filename (str): The file to save.
        """
        LOG.info("Save configuration file %s.", filename)
        os.makedirs(os.path.dirname(filename), exist_ok=True)

        with open(filename, 'w', encoding='utf-8') as file_handle:
            self.config.write(file_handle)

    def __str__(self):
        """Get a string representation of the current configuration.

        Return:
            Returns the string representation.
        """
        ret = ''
        for section in sorted(self.config.sections()):
            if ret:
                ret += '\n'

            ret += f'{section}:\n'
            for option in sorted(self.config[section]):
                env_key = self.env_keys.get(section.upper(), {}).get(option.upper())
                if env_key:
                    ret += f' {option : <20} = {self.config[section][option] : <20} ({env_key})\n'
                else:
                    ret += f' {option : <20} = {self.config[section][option]}\n'
        return ret


# -----------------------------------------------------------------------------
# EOF
# -----------------------------------------------------------------------------
