# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.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)
__metaclass__ = type

import fnmatch
import os
import re
import itertools

from ansible import constants as C
from ansible.errors import AnsibleError, AnsibleOptionsError, AnsibleParserError
from ansible.inventory.data import InventoryData
from ansible.module_utils.six import string_types
from ansible.module_utils._text import to_bytes, to_native
from ansible.parsing.utils.addresses import parse_address
from ansible.plugins.loader import PluginLoader
from ansible.utils.path import unfrackpath

try:
    from __main__ import display
except ImportError:
    from ansible.utils.display import Display
    display = Display()

IGNORED_ALWAYS = [b"^\.", b"^host_vars$", b"^group_vars$", b"^vars_plugins$"]
IGNORED_PATTERNS = [to_bytes(x) for x in C.INVENTORY_IGNORE_PATTERNS]
IGNORED_EXTS = [b'%s$' % to_bytes(re.escape(x)) for x in C.INVENTORY_IGNORE_EXTS]

IGNORED = re.compile(b'|'.join(IGNORED_ALWAYS + IGNORED_PATTERNS + IGNORED_EXTS))


def order_patterns(patterns):
    ''' takes a list of patterns and reorders them by modifier to apply them consistently '''

    # FIXME: this goes away if we apply patterns incrementally or by groups
    pattern_regular = []
    pattern_intersection = []
    pattern_exclude = []
    for p in patterns:
        if p.startswith("!"):
            pattern_exclude.append(p)
        elif p.startswith("&"):
            pattern_intersection.append(p)
        elif p:
            pattern_regular.append(p)

    # if no regular pattern was given, hence only exclude and/or intersection
    # make that magically work
    if pattern_regular == []:
        pattern_regular = ['all']

    # when applying the host selectors, run those without the "&" or "!"
    # first, then the &s, then the !s.
    return pattern_regular + pattern_intersection + pattern_exclude


def split_host_pattern(pattern):
    """
    Takes a string containing host patterns separated by commas (or a list
    thereof) and returns a list of single patterns (which may not contain
    commas). Whitespace is ignored.

    Also accepts ':' as a separator for backwards compatibility, but it is
    not recommended due to the conflict with IPv6 addresses and host ranges.

    Example: 'a,b[1], c[2:3] , d' -> ['a', 'b[1]', 'c[2:3]', 'd']
    """

    if isinstance(pattern, list):
        return list(itertools.chain(*map(split_host_pattern, pattern)))

    # If it's got commas in it, we'll treat it as a straightforward
    # comma-separated list of patterns.

    elif ',' in pattern:
        patterns = re.split('\s*,\s*', pattern)

    # If it doesn't, it could still be a single pattern. This accounts for
    # non-separator uses of colons: IPv6 addresses and [x:y] host ranges.
    else:
        try:
            (base, port) = parse_address(pattern, allow_ranges=True)
            patterns = [pattern]
        except:
            # The only other case we accept is a ':'-separated list of patterns.
            # This mishandles IPv6 addresses, and is retained only for backwards
            # compatibility.
            patterns = re.findall(
                r'''(?:             # We want to match something comprising:
                        [^\s:\[\]]  # (anything other than whitespace or ':[]'
                        |           # ...or...
                        \[[^\]]*\]  # a single complete bracketed expression)
                    )+              # occurring once or more
                ''', pattern, re.X
            )

    return [p.strip() for p in patterns]


class InventoryManager(object):
    ''' Creates and manages inventory '''

    def __init__(self, loader, sources=None):

        # base objects
        self._loader = loader
        self._inventory = InventoryData()

        # a list of host(names) to contain current inquiries to
        self._restriction = None
        self._subset = None

        # caches
        self._hosts_patterns_cache = {}  # resolved full patterns
        self._pattern_cache = {}  # resolved individual patterns
        self._inventory_plugins = []  # for generating inventory

        # the inventory dirs, files, script paths or lists of hosts
        if sources is None:
            self._sources = []
        elif isinstance(sources, string_types):
            self._sources = [sources]
        else:
            self._sources = sources

        # get to work!
        self.parse_sources()

    @property
    def localhost(self):
        return self._inventory.localhost

    @property
    def groups(self):
        return self._inventory.groups

    @property
    def hosts(self):
        return self._inventory.hosts

    def get_vars(self, *args, **kwargs):
        return self._inventory.get_vars(args, kwargs)

    def add_host(self, host, group=None, port=None):
        return self._inventory.add_host(host, group, port)

    def add_group(self, group):
        return self._inventory.add_group(group)

    def get_groups_dict(self):
        return self._inventory.get_groups_dict()

    def reconcile_inventory(self):
        self.clear_caches()
        return self._inventory.reconcile_inventory()

    def get_host(self, hostname):
        return self._inventory.get_host(hostname)

    def _setup_inventory_plugins(self):
        ''' sets up loaded inventory plugins for usage '''

        inventory_loader = PluginLoader('InventoryModule', 'ansible.plugins.inventory', C.DEFAULT_INVENTORY_PLUGIN_PATH, 'inventory_plugins')
        display.vvvv('setting up inventory plugins')

        for name in C.INVENTORY_ENABLED:
            plugin = inventory_loader.get(name)
            if plugin:
                self._inventory_plugins.append(plugin)
            else:
                display.warning('Failed to load inventory plugin, skipping %s' % name)

        if not self._inventory_plugins:
            raise AnsibleError("No inventory plugins available to generate inventory, make sure you have at least one whitelisted.")

    def parse_sources(self, cache=False):
        ''' iterate over inventory sources and parse each one to populate it'''

        self._setup_inventory_plugins()

        parsed = False
        # allow for multiple inventory parsing
        for source in self._sources:

            if source:
                if ',' not in source:
                    source = unfrackpath(source, follow=False)
                parse = self.parse_source(source, cache=cache)
                if parse and not parsed:
                    parsed = True

        if parsed:
            # do post processing
            self._inventory.reconcile_inventory()
        else:
            display.warning("No inventory was parsed, only implicit localhost is available")

        self._inventory_plugins = []

    def parse_source(self, source, cache=False):
        ''' Generate or update inventory for the source provided '''

        parsed = False
        display.debug(u'Examining possible inventory source: %s' % source)

        b_source = to_bytes(source)
        # process directories as a collection of inventories
        if os.path.isdir(b_source):
            display.debug(u'Searching for inventory files in directory: %s' % source)
            for i in sorted(os.listdir(b_source)):

                display.debug(u'Considering %s' % i)
                # Skip hidden files and stuff we explicitly ignore
                if IGNORED.search(i):
                    continue

                # recursively deal with directory entries
                fullpath = os.path.join(b_source, i)
                parsed_this_one = self.parse_source(to_native(fullpath))
                display.debug(u'parsed %s as %s' % (fullpath, parsed_this_one))
                if not parsed:
                    parsed = parsed_this_one
        else:
            # left with strings or files, let plugins figure it out

            # set so new hosts can use for inventory_file/dir vasr
            self._inventory.current_source = source

            # get inventory plugins if needed, there should always be at least one generator
            if not self._inventory_plugins:
                self._setup_inventory_plugins()

            # try source with each plugin
            failures = []
            for plugin in self._inventory_plugins:
                plugin_name = to_native(getattr(plugin, '_load_name', getattr(plugin, '_original_path', '')))
                display.debug(u'Attempting to use plugin %s (%s)' % (plugin_name, plugin._original_path))

                # initialize
                if plugin.verify_file(source):
                    try:
                        plugin.parse(self._inventory, self._loader, source, cache=cache)
                        parsed = True
                        display.vvv('Parsed %s inventory source with %s plugin' % (to_native(source), plugin_name))
                        break
                    except AnsibleParserError as e:
                        display.debug('%s did not meet %s requirements' % (to_native(source), plugin_name))
                        failures.append({'src': source, 'plugin': plugin_name, 'exc': e})
                else:
                    display.debug('%s did not meet %s requirements' % (to_native(source), plugin_name))
            else:
                if not parsed and failures:
                    # only if no plugin processed files should we show errors.
                    if C.INVENTORY_UNPARSED_IS_FAILED:
                        msg = "Could not parse inventory source %s with available plugins:\n" % source
                        for fail in failures:
                            msg += 'Plugin %s failed: %s\n' % (fail['plugin'], to_native(fail['exc']))
                            if display.verbosity >= 3:
                                msg += "%s\n" % fail['exc'].tb
                        raise AnsibleParserError(msg)
                    else:
                        for fail in failures:
                            display.warning('\n* Failed to parse %s with %s plugin: %s' % (to_native(fail['src']), fail['plugin'], to_native(fail['exc'])))
                            display.vvv(fail['exc'].tb)

        if not parsed:
            display.warning("Unable to parse %s as an inventory source" % to_native(source))

        # clear up, jic
        self._inventory.current_source = None

        return parsed

    def clear_caches(self):
        ''' clear all caches '''
        self._hosts_patterns_cache = {}
        self._pattern_cache = {}
        # FIXME: flush inventory cache

    def refresh_inventory(self):
        ''' recalculate inventory '''

        self.clear_caches()
        self._inventory = InventoryData()
        self.parse_sources(cache=False)

    def _match_list(self, items, pattern_str):
        # compile patterns
        try:
            if not pattern_str.startswith('~'):
                pattern = re.compile(fnmatch.translate(pattern_str))
            else:
                pattern = re.compile(pattern_str[1:])
        except Exception:
            raise AnsibleError('Invalid host list pattern: %s' % pattern_str)

        # apply patterns
        results = []
        for item in items: