'''
All salt configuration loading and defaults should be in this module
'''

# Import python modules
import glob
import os
import socket
import logging

# import third party libs
import yaml
try:
    yaml.Loader = yaml.CLoader
    yaml.Dumper = yaml.CDumper
except Exception:
    pass

# Import salt libs
import salt.crypt
import salt.loader
import salt.utils
import salt.pillar
from salt.exceptions import SaltClientError

log = logging.getLogger(__name__)

__dflt_log_datefmt = '%Y-%m-%d %H:%M:%S'
__dflt_log_fmt_console = '[%(levelname)-8s] %(message)s'
__dflt_log_fmt_logfile = '%(asctime)s,%(msecs)03.0f [%(name)-17s][%(levelname)-8s] %(message)s'


def _validate_file_roots(file_roots):
    '''
    If the file_roots option has a key that is None then we will error out,
    just replace it with an empty list
    '''
    if not isinstance(file_roots, dict):
        log.warning('The file_roots parameter is not properly formatted,'
                    ' using defaults')
        return {'base': ['/srv/salt']}
    for env, dirs in list(file_roots.items()):
        if not isinstance(dirs, list) and not isinstance(dirs, tuple):
            file_roots[env] = []
    return file_roots


def _append_domain(opts):
    '''
    Append a domain to the existing id if it doesn't already exist
    '''
    # Domain already exists
    if opts['id'].endswith(opts['append_domain']):
        return opts['id']
    # Trailing dot should mean an FQDN that is terminated, leave it alone.
    if opts['id'].endswith('.'):
        return opts['id']
    return "{0[id]}.{0[append_domain]}".format(opts)


def _read_conf_file(path):
    with open(path, 'r') as conf_file:
        conf_opts = yaml.safe_load(conf_file.read()) or {}
        # allow using numeric ids: convert int to string
        if 'id' in conf_opts:
            conf_opts['id'] = str(conf_opts['id'])
        return conf_opts


def load_config(opts, path, env_var):
    '''
    Attempts to update ``opts`` dict by parsing either the file described by
    ``path`` or the environment variable described by ``env_var`` as YAML.
    '''
    if not path or not os.path.isfile(path):
        path = os.environ.get(env_var, path)
    # If the configuration file is missing, attempt to copy the template,
    # after removing the first header line.
    if not os.path.isfile(path):
        template = '{0}.template'.format(path)
        if os.path.isfile(template):
            with open(path, 'w') as out:
                with open(template, 'r') as f:
                    f.readline()  # skip first line
                    out.write(f.read())

    if os.path.isfile(path):
        try:
            opts.update(_read_conf_file(path))
            opts['conf_file'] = path
        except Exception as e:
            import salt.log
            msg = 'Error parsing configuration file: {0} - {1}'
            if salt.log.is_console_configured():
                log.warn(msg.format(path, e))
            else:
                print(msg.format(path, e))
    else:
        log.debug('Missing configuration file: {0}'.format(path))


def include_config(include, opts, orig_path, verbose):
    '''
    Parses extra configuration file(s) specified in an include list in the
    main config file.
    '''
    # Protect against empty option
    if not include:
        return opts

    if isinstance(include, str):
        include = [include]

    for path in include:
        if not os.path.isabs(path):
            path = os.path.join(os.path.dirname(orig_path), path)

        # Catch situation where user typos path in config; also warns for
        # empty include dir (which might be by design)
        if len(glob.glob(path)) == 0:
            msg = "Warning parsing configuration file: 'include' path/glob '{0}' matches no files"
            if verbose: log.warn(msg.format(path))

        for fn_ in glob.glob(path):
            try:
                opts.update(_read_conf_file(fn_))
            except Exception as e:
                msg = 'Error parsing configuration file: {0} - {1}'
                log.warn(msg.format(fn_, e))
    return opts


def prepend_root_dir(opts, path_options):
    '''
    Prepends the options that represent filesystem paths with value of the
    'root_dir' option.
    '''
    root_dir = os.path.abspath(opts['root_dir'])
    for path_option in path_options:
        if path_option in opts:
            if opts[path_option].startswith(opts['root_dir']):
                opts[path_option] = opts[path_option][len(opts['root_dir']):]
            opts[path_option] = salt.utils.path_join(root_dir, opts[path_option])


def minion_config(path):
    '''
    Reads in the minion configuration file and sets up special options
    '''
    opts = {'master': 'salt',
            'master_port': '4506',
            'master_finger': '',
            'user': 'root',
            'root_dir': '/',
            'pki_dir': '/etc/salt/pki',
            'id': socket.getfqdn(),
            'cachedir': '/var/cache/salt',
            'cache_jobs': False,
            'conf_file': path,
            'sock_dir': '/var/run/salt',
            'backup_mode': '',
            'renderer': 'yaml_jinja',
            'failhard': False,
            'autoload_dynamic_modules': True,
            'environment': None,
            'state_top': 'top.sls',
            'startup_states': '',
            'sls_list': [],
            'top_file': '',
            'file_client': 'remote',
            'file_roots': {
                'base': ['/srv/salt'],
                },
            'pillar_roots': {
                'base': ['/srv/pillar'],
                },
            'hash_type': 'md5',
            'external_nodes': '',
            'disable_modules': [],
            'disable_returners': [],
            'module_dirs': [],
            'returner_dirs': [],
            'states_dirs': [],
            'render_dirs': [],
            'providers': {},
            'clean_dynamic_modules': True,
            'open_mode': False,
            'multiprocessing': True,
            'sub_timeout': 60,
            'ipc_mode': 'ipc',
            'tcp_pub_port': 4510,
            'tcp_pull_port': 4511,
            'log_file': '/var/log/salt/minion',
            'log_level': None,
            'log_level_logfile': None,
            'log_datefmt': __dflt_log_datefmt,
            'log_fmt_console': __dflt_log_fmt_console,
            'log_fmt_logfile': __dflt_log_fmt_logfile,
            'log_granular_levels': {},
            'test': False,
            'cython_enable': False,
            'state_verbose': True,
            'state_output': 'full',
            'acceptance_wait_time': 10,
            'dns_check': True,
            'verify_env': True,
            'grains': {},
            'permissive_pki_access': False,
            'default_include': 'minion.d/*.conf',
            'update_url': False,
            'update_restart_services': [],
            }

    if len(opts['sock_dir']) > len(opts['cachedir']) + 10:
        opts['sock_dir'] = os.path.join(opts['cachedir'], '.salt-unix')

    load_config(opts, path, 'SALT_MINION_CONFIG')

    default_include = opts.get('default_include', [])
    include = opts.get('include', [])

    opts = include_config(default_include, opts, path, verbose=False)
    opts = include_config(include, opts, path, verbose=True)

    if 'append_domain' in opts:
        opts['id'] = _append_domain(opts)

    try:
        opts['master_ip'] = salt.utils.dns_check(opts['master'], True)
    except SaltClientError:
        opts['master_ip'] = '127.0.0.1'

    opts['master_uri'] = 'tcp://{ip}:{port}'.format(ip=opts['master_ip'],
                                                    port=opts['master_port'])

    # Enabling open mode requires that the value be set to True, and
    # nothing else!
    opts['open_mode'] = opts['open_mode'] is True

    # set up the extension_modules location from the cachedir
    opts['extension_modules'] = (
            opts.get('extension_modules') or 
            os.path.join(opts['cachedir'], 'extmods')
            )

    # Prepend root_dir to other paths
    prepend_root_dir(opts, ['pki_dir', 'cachedir', 'log_file', 'sock_dir',
                            'key_logfile', 'extension_modules'])
    return opts


def master_config(path):
    '''
    Reads in the master configuration file and sets up default options
    '''
    opts = {'interface': '0.0.0.0',
            'publish_port': '4505',
            'user': 'root',
            'worker_threads': 5,
            'sock_dir': '/var/run/salt',
            'ret_port': '4506',
            'timeout': 5,
            'keep_jobs': 24,
            'root_dir': '/',
            'pki_dir': '/etc/salt/pki',
            'cachedir': '/var/cache/salt',
            'file_roots': {
                'base': ['/srv/salt'],
                },
            'master_roots': {
                'base': ['/srv/salt-master'],
                },
            'pillar_roots': {
                'base': ['/srv/pillar'],
                },
            'ext_pillar': [],
            # TODO - Set this to 2 by default in 0.10.5
            'pillar_version': 1,
            'syndic_master': '',
            'runner_dirs': [],
            'client_acl': {},
            'external_auth': {},
            'token_expire': 720,
            'file_buffer_size': 1048576,
            'max_open_files': 100000,
            'hash_type': 'md5',
            'conf_file': path,
            'pub_refresh': False,
            'open_mode': False,
            'auto_accept': False,
            'renderer': 'yaml_jinja',
            'failhard': False,
            'state_top': 'top.sls',
            'master_tops': {},
            'external_nodes': '',
            'order_masters': False,
            'job_cache': True,
            'minion_data_cache': True,
            'log_file': '/var/log/salt/master',
            'log_level': None,
            'log_level_logfile': None,
            'log_datefmt': __dflt_log_datefmt,
            'log_fmt_console': __dflt_log_fmt_console,
            'log_fmt_logfile': __dflt_log_fmt_logfile,
            'log_granular_levels': {},
            'pidfile': '/var/run/salt-master.pid',
            'cluster_masters': [],
            'cluster_mode': 'paranoid',
            'range_server': 'range:80',
            'serial': 'msgpack',
            'state_verbose': True,
            'state_output': 'full',
            'nodegroups': {},
            'cython_enable': False,
            'key_logfile': '/var/log/salt/key',
            'verify_env': True,
            'permissive_pki_access': False,
            'default_include': 'master.d/*.conf',
    }

    if len(opts['sock_dir']) > len(opts['cachedir']) + 10:
        opts['sock_dir'] = os.path.join(opts['cachedir'], '.salt-unix')

    load_config(opts, path, 'SALT_MASTER_CONFIG')

    default_include = opts.get('default_include', [])
    include = opts.get('include', [])

    opts = include_config(default_include, opts, path, verbose=False)
    opts = include_config(include, opts, path, verbose=True)

    opts['aes'] = salt.crypt.Crypticle.generate_key_string()

    opts['extension_modules'] = (
            opts.get('extension_modules') or 
            os.path.join(opts['cachedir'], 'extmods')
            )
    opts['token_dir'] = os.path.join(opts['cachedir'], 'tokens')
    # Prepend root_dir to other paths
    prepend_root_dir(opts, ['pki_dir', 'cachedir', 'log_file',
                            'sock_dir', 'key_logfile', 'extension_modules',
                            'autosign_file', 'token_dir'])

    # Enabling open mode requires that the value be set to True, and
    # nothing else!
    opts['open_mode'] = opts['open_mode'] is True
    opts['auto_accept'] = opts['auto_accept'] is True
    opts['file_roots'] = _validate_file_roots(opts['file_roots'])
    return opts


def client_config(path):
    '''
    Load in the configuration data needed for the LocalClient. This function
    searches for client specific configurations and adds them to the data from
    the master configuration.
    '''
    opts = {'token_file': os.path.expanduser('~/.salt_token')}
    opts.update(master_config(path))
    cpath = os.path.expanduser('~/.salt')
    load_config(opts, cpath, 'SALT_CLIENT_CONFIG')
    if 'token_file' in opts:
        opts['token_file'] = os.path.expanduser(opts['token_file'])
    if os.path.isfile(opts['token_file']):
        with open(opts['token_file']) as fp_:
            opts['token'] = fp_.read().strip()
    return opts
