# (c) 2015, Kevin Carter <kevin.carter@rackspace.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import (absolute_import, division, print_function)

try:
    import ConfigParser
except ImportError:
    import configparser as ConfigParser
import datetime
try:
    from StringIO import StringIO
except ImportError:
    from io import StringIO
import json
import os
import pwd
import re
import six
import time
import yaml
import tempfile as tmpfilelib

from ansible.plugins.action import ActionBase
from ansible.module_utils._text import to_bytes, to_text
from ansible import constants as C
from ansible import errors
from ansible.parsing.yaml.dumper import AnsibleDumper
from distutils.version import LooseVersion
from ansible import __version__ as __ansible_version__

__metaclass__ = type


CONFIG_TYPES = {
    'ini': 'return_config_overrides_ini',
    'json': 'return_config_overrides_json',
    'yaml': 'return_config_overrides_yaml'
}


class IDumper(AnsibleDumper):
    def increase_indent(self, flow=False, indentless=False):
        return super(IDumper, self).increase_indent(flow, False)


class MultiKeyDict(dict):
    """Dictionary class which supports duplicate keys.
    This class allows for an item to be added into a standard python dictionary
    however if a key is created more than once the dictionary will convert the
    singular value to a python tuple. This tuple type forces all values to be a
    string.
    Example Usage:
    >>> z = MultiKeyDict()
    >>> z['a'] = 1
    >>> z['b'] = ['a', 'b', 'c']
    >>> z['c'] = {'a': 1}
    >>> print(z)
    ... {'a': 1, 'b': ['a', 'b', 'c'], 'c': {'a': 1}}
    >>> z['a'] = 2
    >>> print(z)
    ... {'a': tuple(['1', '2']), 'c': {'a': 1}, 'b': ['a', 'b', 'c']}
    """

    def __setitem__(self, key, value):
        if key in self:
            if isinstance(self[key], tuple):
                items = self[key]
                if str(value) not in items:
                    items += tuple([str(value)])
                    super(MultiKeyDict, self).__setitem__(key, items)
            else:
                if str(self[key]) != str(value):
                    items = tuple([str(self[key]), str(value)])
                    super(MultiKeyDict, self).__setitem__(key, items)
        else:
            return dict.__setitem__(self, key, value)


class ConfigTemplateParser(ConfigParser.RawConfigParser):
    """ConfigParser which supports multi key value.
    The parser will use keys with multiple variables in a set as a multiple
    key value within a configuration file.
    Default Configuration file:
    [DEFAULT]
    things =
        url1
        url2
        url3
    other = 1,2,3
    [section1]
    key = var1
    key = var2
    key = var3
    Example Usage:
    >>> cp = ConfigTemplateParser(dict_type=MultiKeyDict)
    >>> cp.read('/tmp/test.ini')
    ... ['/tmp/test.ini']
    >>> cp.get('DEFAULT', 'things')
    ... \nurl1\nurl2\nurl3
    >>> cp.get('DEFAULT', 'other')
    ... '1,2,3'
    >>> cp.set('DEFAULT', 'key1', 'var1')
    >>> cp.get('DEFAULT', 'key1')
    ... 'var1'
    >>> cp.get('section1', 'key')
    ... {'var1', 'var2', 'var3'}
    >>> cp.set('section1', 'key', 'var4')
    >>> cp.get('section1', 'key')
    ... {'var1', 'var2', 'var3', 'var4'}
    >>> with open('/tmp/test2.ini', 'w') as f:
    ...     cp.write(f)
    Output file:
    [DEFAULT]
    things =
        url1
        url2
        url3
    key1 = var1
    other = 1,2,3
    [section1]
    key = var4
    key = var1
    key = var3
    key = var2
    """

    def __init__(self, *args, **kwargs):
        self._comments = {}
        self.ignore_none_type = bool(kwargs.pop('ignore_none_type', True))
        self.default_section = str(kwargs.pop('default_section', 'DEFAULT'))
        ConfigParser.RawConfigParser.__init__(self, *args, **kwargs)

    def _write(self, fp, section, key, item, entry):
        if section:
            # If we are not ignoring a none type value, then print out
            # the option name only if the value type is None.
            if not self.ignore_none_type and item is None:
                fp.write(key + '\n')
            elif (item is not None) or (self._optcre == self.OPTCRE):
                fp.write(entry)
        else:
            fp.write(entry)

    def _write_check(self, fp, key, value, section=False):
        if isinstance(value, (tuple, set)):
            for item in value:
                item = str(item).replace('\n', '\n\t')
                entry = "%s = %s\n" % (key, item)
                self._write(fp, section, key, item, entry)
        else:
            if isinstance(value, list):
                _value = [str(i.replace('\n', '\n\t')) for i in value]
                entry = '%s = %s\n' % (key, ','.join(_value))
            else:
                entry = '%s = %s\n' % (key, str(value).replace('\n', '\n\t'))
            self._write(fp, section, key, value, entry)

    def write(self, fp):
        def _do_write(section_name, section, section_bool=False):
            _write_comments(section_name)
            fp.write("[%s]\n" % section_name)
            for key, value in sorted(section.items()):
                _write_comments(section_name, optname=key)
                self._write_check(fp, key=key, value=value,
                                  section=section_bool)
            else:
                fp.write("\n")

        def _write_comments(section, optname=None):
            comsect = self._comments.get(section, {})
            if optname in comsect:
                fp.write(''.join(comsect[optname]))

        if self.default_section != 'DEFAULT' and self._sections.get(
                self.default_section, False):
            _do_write(self.default_section,
                      self._sections[self.default_section],
                      section_bool=True)
            self._sections.pop(self.default_section)

        if self._defaults:
            _do_write('DEFAULT', self._defaults)

        for section in sorted(self._sections):
            _do_write(section, self._sections[section], section_bool=True)

    def _read(self, fp, fpname):
        comments = []
        cursect = None
        optname = None
        lineno = 0
        e = None
        while True:
            line = fp.readline()
            if not line:
                break
            lineno += 1
            if line.strip() == '':
                if comments:
                    comments.append('')
                continue

            if line[0] in '#;':
                comments.append(line)
                continue

            if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
                continue
            if line[0].isspace() and cursect is not None and optname:
                value = line.strip()
                if value:
                    try:
                        if isinstance(cursect[optname], (tuple, set)):
                            _temp_item = list(cursect[optname])
                            del cursect[optname]
                            cursect[optname] = _temp_item
                        elif isinstance(cursect[optname], six.text_type):
                            _temp_item = [cursect[optname]]
                            del cursect[optname]
                            cursect[optname] = _temp_item
                    except NameError:
                        if isinstance(cursect[optname], (bytes, str)):
                            _temp_item = [cursect[optname]]
                            del cursect[optname]
                            cursect[optname] = _temp_item
                    cursect[optname].append(value)
            else:
                mo = self.SECTCRE.match(line)
                if mo:
                    sectname = mo.group('header')
                    if sectname in self._sections:
                        cursect = self._sections[sectname]
                    elif sectname == 'DEFAULT':
                        cursect = self._defaults
                    else:
                        cursect = self._dict()
                        self._sections[sectname] = cursect
                    optname = None

                    comsect = self._comments.setdefault(sectname, {})
                    if comments:
                        # NOTE(flaper87): Using none as the key for
                        # section level comments
                        comsect[None] = comments
                        comments = []
                elif cursect is None:
                    raise ConfigParser.MissingSectionHeaderError(
                        fpname,
                        lineno,
                        line
                    )
                else:
                    mo = self._optcre.match(line)
                    if mo:
                        optname, vi, optval = mo.group('option', 'vi', 'value')
                        optname = self.optionxform(optname.rstrip())
                        if optval is not None:
                            if vi in ('=', ':') and ';' in optval:
                                pos = optval.find(';')
                                if pos != -1 and optval[pos - 1].isspace():
                                    optval = optval[:pos]
                            optval = optval.strip()
                            if optval == '""':
                                optval = ''
                        cursect[optname] = optval
                        if comments:
                            comsect[optname] = comments
                            comments = []
                    else:
                        if not e:
                            e = ConfigParser.ParsingError(fpname)
                        e.append(lineno, repr(line))
        if e:
            raise e
        all_sections = [self._defaults]
        all_sections.extend(self._sections.values())
        for options in all_sections:
            for name, val in options.items():
                if isinstance(val, list):
                    _temp_item = '\n'.join(val)
                    del options[name]
                    options[name] = _temp_item


class ActionModule(ActionBase):
    TRANSFERS_FILES = True

    def return_config_overrides_ini(self,
                                    config_overrides,
                                    resultant,
                                    list_extend=True,
                                    ignore_none_type=True,
                                    default_section='DEFAULT'):
        """Returns string value from a modified config file.

        :param config_overrides: ``dict``
        :param resultant: ``str`` || ``unicode``
        :returns: ``str``
        """
        # If there is an exception loading the RawConfigParser The config obj
        #  is loaded again without the extra option. This is being done to
        #  support older python.
        try:
            config = ConfigTemplateParser(
                allow_no_value=True,
                dict_type=MultiKeyDict,
                ignore_none_type=ignore_none_type,
                default_section=default_section
            )
            config.optionxform = str
        except Exception:
            config = ConfigTemplateParser(dict_type=MultiKeyDict)

        config_object = StringIO(resultant)
        config.readfp(config_object)
        for section, items in config_overrides.items():
            # If the items value is not a dictionary it is assumed that the
            #  value is a default item for this config type.
            if not isinstance(items, dict):
                if isinstance(items, list):
                    items = ','.join(to_text(i) for i in items)
                self._option_write(
                    config,
                    'DEFAULT',
                    section,
                    items
                )
            else:
                # Attempt to add a section to the config file passing if
                #  an error is raised that is related to the section
                #  already existing.
                try:
                    config.add_section(section)
                except (ConfigParser.DuplicateSectionError, ValueError):
                    pass
                for key, value in items.items():
                    try:
                        self._option_write(config, section, key, value)
                    except ConfigParser.NoSectionError as exp:
                        error_msg = str(exp)
                        error_msg += (
                            ' Try being more explicit with your override'
                            'data. Sections are case sensitive.'
                        )
                        raise errors.AnsibleModuleError(error_msg)
        else:
            config_object.close()

        resultant_stringio = StringIO()
        try:
            config.write(resultant_stringio)
            return resultant_stringio.getvalue()
        finally:
            resultant_stringio.close()

    @staticmethod
    def _option_write(config, section, key, value):
        config.remove_option(str(section), str(key))
        try:
            if not any(list(value.values())):
                value = tuple(value.keys())
        except AttributeError:
            pass
        if isinstance(value, (tuple, set)):
            config.set(str(section), str(key), value)
        elif isinstance(value, set):
            config.set(str(section), str(key), value)
        elif isinstance(value, list):
            config.set(str(section), str(key), ','.join(str(i) for i in value))
        else:
            config.set(str(section), str(key), str(value))

    def return_config_overrides_json(self,
                                     config_overrides,
                                     resultant,
                                     list_extend=True,
                                     ignore_none_type=True,
                                     default_section='DEFAULT'):
        """Returns config json