"""Expanding substitutions in the configuration file."""

import os
import random
import re
import time
from typing import Callable, Dict, Optional
from configparser import ConfigParser
import ldap # type:ignore
from arcnagios.nagutils import ServiceUnknown
from arcnagios.reputation import ReputationTracker

Environment = Dict[str, str]

SubstitutionFunction = \
    Callable[[ConfigParser, str, str, ReputationTracker, Environment], None]

def get_interp_opt(config: ConfigParser, section: str, var: str,
                   reputation_tracker: ReputationTracker,
                   target_env: Environment) -> Optional[str]:
    if config.has_option(section, var):
        import_interpolated_variables(
                config, section, var, reputation_tracker, target_env)
        return config.get(section, var, vars = target_env)
    return None

def get_interp(config: ConfigParser, section: str, var: str,
               reputation_tracker: ReputationTracker,
               target_env: Environment,
               default: Optional[str] = None) -> str:
    value = \
        get_interp_opt(config, section, var, reputation_tracker, target_env) \
        or default
    if value is None:
        raise ServiceUnknown('Configuration error: Missing %s in [%s].'
                             % (var, section))
    return value

def _subst_option(config: ConfigParser, section: str, var: str,
                  reputation_tracker: ReputationTracker,
                  target_env: Environment) -> None:
    default = get_interp_opt(
            config, section, 'default', reputation_tracker, target_env)
    if default is None:
        raise ServiceUnknown('Missing required option (-O %s=...).'%var)
    target_env[var] = default

def _subst_getenv(config: ConfigParser, section: str, var: str,
                  reputation_tracker: ReputationTracker,
                  target_env: Environment) -> None:
    default = get_interp_opt(
            config, section, 'default', reputation_tracker, target_env)
    envvar = get_interp_opt(
            config, section, 'envvar', reputation_tracker, target_env)
    if envvar is None:
        prefix = get_interp(
                config, section, 'prefix', reputation_tracker, target_env, '')
        envvar = prefix + var
    v = os.getenv(envvar, default)
    if v is None:
        raise ServiceUnknown('Missing required option (-O) or '
                             'enviroment variable %s.' % envvar)
    target_env[var] = v

def _subst_ldap(config: ConfigParser, section: str, var: str,
                reputation_tracker: ReputationTracker,
                target_env: Environment) -> None:
    basedn = get_interp(
            config, section, 'basedn', reputation_tracker, target_env)
    filterstr = get_interp(
            config, section, 'filter', reputation_tracker, target_env)
    attribute = get_interp(
            config, section, 'attribute', reputation_tracker, target_env)
    attrlist = list(map(str.strip, attribute.split(',')))
    scope = ldap.SCOPE_SUBTREE
    errors = []
    entries = []
    for uri in get_interp(config, section, 'uri',
                          reputation_tracker, target_env).split():
        try:
            conn = ldap.initialize(uri)
            entries = conn.search_s(basedn, scope, filterstr, attrlist=attrlist)
            break
        except (ldap.BUSY,
                ldap.CONNECT_ERROR,
                ldap.OTHER,
                ldap.PROTOCOL_ERROR,
                ldap.TIMEOUT,
                ldap.TIMELIMIT_EXCEEDED) as exn:
            # Exceptions related to presumed server-side issues.
            errors.append('%s gives %s' % (uri, exn))
        except ldap.LDAPError as exn:
            # Exceptions related to presumed client-side issues.
            raise ServiceUnknown('LDAP query to %s from %s failed with: %s'
                    % (uri, section, exn)) from exn
    else:
        raise ServiceUnknown('LDAP query %s failed with: %s' % (section, exn))
    for _, entry in entries:
        for attr in attrlist:
            if attr in entry:
                target_env[var] = entry[attr][0].decode('utf-8')
                return
    if config.has_option(section, 'default'):
        target_env[var] = get_interp(
                config, section, 'default', reputation_tracker, target_env)
    else:
        raise ServiceUnknown('LDAP query %s did not provide a value for %s.'
                             % (filterstr, section))

def _subst_pipe(config: ConfigParser, section: str, var: str,
                reputation_tracker: ReputationTracker,
                target_env: Environment) -> None:
    cmd = get_interp(config, section, 'command', reputation_tracker, target_env)
    fh = os.popen(cmd)
    target_env[var] = fh.read().strip()
    fh.close()

def _subst_random_line(config: ConfigParser, section: str, var: str,
                       reputation_tracker: ReputationTracker,
                       target_env: Environment) -> None:
    path = get_interp(
            config, section, 'input_file', reputation_tracker, target_env)

    include = None
    exclude = set()
    if config.has_option(section, 'exclude'):
        exclude = set(get_interp(
            config, section, 'exclude', reputation_tracker, target_env).split())
    if config.has_option(section, 'include'):
        include = set(get_interp(
            config, section, 'include', reputation_tracker, target_env).split())
        include.difference_update(exclude)

    rng = random.Random(time.time())
    if config.has_option(section, 'reputation_dist'):
        # Load distribution from a file.
        dist_name = config.get(section, 'reputation_dist', vars=target_env)
        with open(path, encoding='utf-8') as fh:
            lines = set(line for line in map(str.strip, fh)
                             if line != '' and line[0] != '#')
        if not include is None:
            lines.intersection_update(include)
        else:
            lines.difference_update(exclude)
        if not lines:
            raise ServiceUnknown('%s must contain at least one non-excluded '
                                 'line' % path)
        target_env[var] = reputation_tracker.choose(dist_name, lines)
    else:
        # Uniform distribution.
        chosen_line = None
        try:
            with open(path, encoding='utf-8') as fh:
                seen_count = 0
                for line in fh:
                    line = line.strip()
                    if not line or line.startswith('#') \
                            or not include is None and not line in include \
                            or include is None and line in exclude:
                        continue
                    if rng.randint(0, seen_count) == 0:
                        chosen_line = line
                    seen_count += 1
        except IOError as exn:
            raise ServiceUnknown(str(exn)) from exn
        if chosen_line is None:
            raise ServiceUnknown('%s must contain at least one non-excluded '
                                 'line' % path)
        target_env[var] = chosen_line

def _subst_strftime(config: ConfigParser, section: str, var: str,
                    reputation_tracker: ReputationTracker,
                    target_env: Environment) -> None:
    if config.has_option(section, 'raw_format'):
        fmt = config.get(section, 'raw_format', vars = target_env, raw=True)
    else:
        fmt = get_interp(
                config, section, 'format', reputation_tracker, target_env)
    target_env[var] = time.strftime(fmt)

def _subst_switch(config: ConfigParser, section: str, var: str,
                  reputation_tracker: ReputationTracker,
                  target_env: Environment) -> None:
    case = 'case[%s]' % get_interp(
            config, section, 'index', reputation_tracker, target_env)
    if config.has_option(section, case):
        import_interpolated_variables(
                config, section, case, reputation_tracker, target_env)
        target_env[var] = get_interp(
                config, section, case, reputation_tracker, target_env)
    else:
        if not config.has_option(section, 'default'):
            raise ServiceUnknown(
                    'No %s and no default in section variable.%s.'%(case, var))
        import_interpolated_variables(
                config, section, 'default', reputation_tracker, target_env)
        target_env[var] = get_interp(
                config, section, 'default', reputation_tracker, target_env)

_METHODS_BY_NAME: Dict[str, SubstitutionFunction] = {
    'getenv': _subst_getenv,
    'ldap': _subst_ldap,
    'option': _subst_option,
    'pipe': _subst_pipe,
    'random_line': _subst_random_line,
    'strftime': _subst_strftime,
    'switch': _subst_switch,
}

def register_substitution_method(
        name: str,
        f: SubstitutionFunction):
    _METHODS_BY_NAME[name] = f

_INTERP_RE = re.compile(r'%\(([a-zA-Z0-9_]+)\)')

def import_interpolated_variables(
        config: ConfigParser, section: str, var: str,
        reputation_tracker: ReputationTracker, target_env: Environment) \
        -> None:
    """Import variables needed for expanding ``var`` in ``section``."""

    raw_value = config.get(section, var, raw = True)
    for mo in re.finditer(_INTERP_RE, raw_value):
        v = mo.group(1)
        if not v in target_env:
            import_variable(config, v, reputation_tracker, target_env)

def import_variable(config: ConfigParser, var: str,
                    reputation_tracker: ReputationTracker,
                    target_env: Environment) -> None:
    """Import ``var`` by executing its defining section, populating
    ``target_env`` with its value and the values of any dependent
    variables."""

    section = 'variable.' + var
    method = config.get(section, 'method')
    try:
        return _METHODS_BY_NAME[method]\
                (config, section, var, reputation_tracker, target_env)
    except KeyError:
        # pylint: disable=W0707
        raise ServiceUnknown('Unknown substitution method %s.' % method)
