"""
HttpdConf - files ``/etc/httpd/conf/httpd.conf`` and ``/etc/httpd/conf.d/*``
============================================================================

Parse the keyword-and-value-but-also-vaguely-XML of an Apache configuration
file.

Generally, each line is split on the first space into key and value, leading
and trailing space being ignored.

Sample (edited) httpd.conf file::

    ServerRoot "/etc/httpd"
    LoadModule auth_basic_module modules/mod_auth_basic.so
    LoadModule auth_digest_module modules/mod_auth_digest.so

    <Directory />
        Options FollowSymLinks
        AllowOverride None
    </Directory>

    <IfModule mod_mime_magic.c>
    #   MIMEMagicFile /usr/share/magic.mime
        MIMEMagicFile conf/magic
    </IfModule>

    ErrorLog "|/usr/sbin/httplog -z /var/log/httpd/error_log.%Y-%m-%d"

    SSLProtocol -ALL +SSLv3
    #SSLProtocol all -SSLv2

    NSSProtocol SSLV3 TLSV1.0
    #NSSProtocol ALL

    # prefork MPM
    <IfModule prefork.c>
    StartServers       8
    MinSpareServers    5
    MaxSpareServers   20
    ServerLimit      256
    MaxClients       256
    MaxRequestsPerChild  200
    </IfModule>

    # worker MPM
    <IfModule worker.c>
    StartServers         4
    MaxClients         300
    MinSpareThreads     25
    MaxSpareThreads     75
    ThreadsPerChild     25
    MaxRequestsPerChild  0
    </IfModule>

Examples:

    >>> httpd_conf['ServerRoot'][-1].value
    '/etc/httpd'
    >>> httpd_conf['LoadModule'][0].value
    'auth_basic_module modules/mod_auth_basic.so'
    >>> httpd_conf['LoadModule'][-1].value
    'auth_digest_module modules/mod_auth_digest.so'
    >>> httpd_conf['Directory', '/']['Options'][-1].value
    'FollowSymLinks'
    >>> type(httpd_conf[('IfModule','prefork.c')]) == type({})
    True
    >>> httpd_conf[('IfModule','mod_mime_magic.c')]
    {'MIMEMagicFile': [ParsedData(value='conf/magic', line='MIMEMagicFile conf/magic', section='IfModule', section_name='mod_mime_magic.c', file_name='path', file_path='/path')]}
    >>> httpd_conf[('IfModule','prefork.c')]['StartServers'][0].value
    '8'
    >>> 'ThreadsPerChild' in httpd_conf[('IfModule','prefork.c')]
    False
    >>> httpd_conf[('IfModule','worker.c')]['MaxRequestsPerChild'][-1].value
    '0'

"""
from collections import namedtuple

import re
from copy import deepcopy
from .. import Parser, parser, get_active_lines, LegacyItemAccess
from insights.specs import Specs
from insights.util import deprecated

ParsedData = namedtuple('ParsedData', ['value', 'line', 'section', 'section_name', 'file_name', 'file_path'])
"""namedtuple: Type for storing the parsed httpd configuration's directive information."""


@parser(Specs.httpd_conf)
class HttpdConf(LegacyItemAccess, Parser):
    """
    .. note::
        This parser is deprecated, please use
        :py:class:`insights.combiners.httpd_conf.HttpdConfTree` instead.

    Get the key value pairs separated on the first space, ignoring leading
    and trailing spaces.

    If the file is ``httpd.conf``, it also stores first half, before
    ``IncludeOptional conf.d/*.conf`` line, and the rest, to the ``first_half``
    and ``second_half`` attributes respectively.

    Attributes:
        data (dict): Dictionary of parsed data with key being the option and value a list of
                     named tuples with the following properties:
                     - ``value`` - the value of the keyword.
                     - ``line`` - the complete line as found in the config file.
                     The reason why it is a list is to store data for directives which can use
                     selective overriding such as ``UserDir``.
        first_half (dict): Parsed data from main config file before inclusion of other files in the
                           same format as ``data``.
        second_half (dict): Parsed data from main config file after inclusion of other files in the
                            same format as ``data``.
    """

    def __init__(self, *args, **kwargs):
        deprecated(HttpdConf, "Import HttpdConfTree from insights.combiner.httpd_conf instead.")
        self.data = {}
        self.first_half = {}
        self.second_half = {}
        super(HttpdConf, self).__init__(*args, **kwargs)

    def parse_content(self, content):
        def add_to_dict_list(dictionary, key, element):
            """
            Utility function to create a dictionary of lists instead of using defaultdict, because
            rule would be able to unknowingly modify defaultdict structures.

            Args:
                dictionary (dict): The changed dictionary.
                key (str): The dictionary key to be changed. If it is in the dictionary, ``element``
                           is going to be appended to ``dictionary[key]`` list. If the ``key`` is
                           not in the dictionary, it is created so that
                           ``dictionary[key] = [element]``.
                element (Object): A value to be appended to the list under ``dictionary[key]``.
            """
            if key not in dictionary:
                dictionary[key] = [element]
            else:
                dictionary[key].append(element)

        where_to_store = self.first_half  # Set which part of file is the parser at

        # Flag to be used for different parsing of the main config file
        main_config = self.file_name == 'httpd.conf'

        section = []  # Can be treated as a stack
        for line in get_active_lines(content):
            if main_config and where_to_store is not self.second_half:
                # Dividing line looks like 'IncludeOptional conf.d/*.conf'
                if re.search(r'^\s*IncludeOptional\s+conf\.d', line):
                    where_to_store = self.second_half

            # new section start
            if line.startswith('<') and not line.startswith('</'):
                splits = line.strip('<>').split(None, 1)
                section.append(((splits[0], splits[1] if len(splits) == 2 else ''), {}))
            # one section end
            elif line.startswith('</'):
                sec, pd = section.pop()
                # for nested section
                if section:
                    if sec not in section[-1][-1]:
                        section[-1][-1][sec] = {}
                    dict_deep_merge(section[-1][-1][sec], pd)
                else:
                    if sec not in self.data:
                        self.data[sec] = {}
                        if main_config:
                            where_to_store[sec] = {}
                    dict_deep_merge(self.data[sec], pd)
                    if main_config:
                        dict_deep_merge(where_to_store[sec], pd)
            else:
                try:
                    option, value = [s.strip() for s in line.split(None, 1)]
                except ValueError:
                    continue  # Skip lines which are not 'Option Value'

                value = value.strip('\'"')

                if section:
                    cur_sec = section[-1][0]
                    parsed_data = ParsedData(value, line, cur_sec[0], cur_sec[1], self.file_name, self.file_path)
                    # before: section = [(('IfModule', 'worker.c'), {})]
                    add_to_dict_list(section[-1][-1], option, parsed_data)
                    # after:  section = [(('IfModule', 'worker.c'), [{'MaxClients': (256, 'MaxClients 256')}])]
                else:
                    parsed_data = ParsedData(value, line, None, None, self.file_name, self.file_path)
                    add_to_dict_list(self.data, option, parsed_data)
                    if main_config:
                        add_to_dict_list(where_to_store, option, parsed_data)


def dict_deep_merge(tgt, src):
    """
    Utility function to merge the source dictionary `src` to the target
    dictionary recursively

    Note:
        The type of the values in the dictionary can only be `dict` or `list`

    Parameters:
        tgt (dict): The target dictionary
        src (dict): The source dictionary
    """
    for k, v in src.items():
        if k in tgt:
            if isinstance(tgt[k], dict) and isinstance(v, dict):
                dict_deep_merge(tgt[k], v)
            else:
                tgt[k].extend(deepcopy(v))
        else:
            tgt[k] = deepcopy(v)
