# Copyright (c) 2018 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import annotations

import os

from functools import lru_cache

from ansible import constants as C
from ansible.errors import AnsibleError
from ansible.inventory.group import InventoryObjectType
from ansible.plugins.loader import vars_loader
from ansible.utils.display import Display
from ansible.utils.vars import combine_vars

display = Display()

cached_vars_plugin_order = None


def _load_vars_plugins_order():
    # find 3rd party legacy vars plugins once, and look them up by name subsequently
    auto = []
    for auto_run_plugin in vars_loader.all(class_only=True):
        needs_enabled = False
        if hasattr(auto_run_plugin, 'REQUIRES_ENABLED'):
            needs_enabled = auto_run_plugin.REQUIRES_ENABLED
        elif hasattr(auto_run_plugin, 'REQUIRES_WHITELIST'):
            needs_enabled = auto_run_plugin.REQUIRES_WHITELIST
            display.deprecated("The VarsModule class variable 'REQUIRES_WHITELIST' is deprecated. "
                               "Use 'REQUIRES_ENABLED' instead.", version=2.18)
        if needs_enabled:
            continue
        auto.append(auto_run_plugin._load_name)

    # find enabled plugins once so we can look them up by resolved fqcn subsequently
    enabled = []
    for plugin_name in C.VARIABLE_PLUGINS_ENABLED:
        if (plugin := vars_loader.get(plugin_name)) is None:
            enabled.append(plugin_name)
        else:
            collection = '.' in plugin.ansible_name and not plugin.ansible_name.startswith('ansible.builtin.')
            # Warn if a collection plugin has REQUIRES_ENABLED because it has no effect.
            if collection and (hasattr(plugin, 'REQUIRES_ENABLED') or hasattr(plugin, 'REQUIRES_WHITELIST')):
                display.warning(
                    "Vars plugins in collections must be enabled to be loaded, REQUIRES_ENABLED is not supported. "
                    "This should be removed from the plugin %s." % plugin.ansible_name
                )
            enabled.append(plugin.ansible_name)

    global cached_vars_plugin_order
    cached_vars_plugin_order = auto + enabled


def get_plugin_vars(loader, plugin, path, entities):

    data = {}
    try:
        data = plugin.get_vars(loader, path, entities)
    except AttributeError:
        if hasattr(plugin, 'get_host_vars') or hasattr(plugin, 'get_group_vars'):
            display.deprecated(
                f"The vars plugin {plugin.ansible_name} from {plugin._original_path} is relying "
                "on the deprecated entrypoints 'get_host_vars' and 'get_group_vars'. "
                "This plugin should be updated to inherit from BaseVarsPlugin and define "
                "a 'get_vars' method as the main entrypoint instead.",
                version="2.20",
            )
        try:
            for entity in entities:
                if entity.base_type is InventoryObjectType.HOST:
                    data |= plugin.get_host_vars(entity.name)
                else:
                    data |= plugin.get_group_vars(entity.name)
        except AttributeError:
            if hasattr(plugin, 'run'):
                raise AnsibleError("Cannot use v1 type vars plugin %s from %s" % (plugin._load_name, plugin._original_path))
            else:
                raise AnsibleError("Invalid vars plugin %s from %s" % (plugin._load_name, plugin._original_path))
    return data


# optimized for stateless plugins; non-stateless plugin instances will fall out quickly
@lru_cache(maxsize=10)
def _plugin_should_run(plugin, stage):
    # if a plugin-specific setting has not been provided, use the global setting
    # older/non shipped plugins that don't support the plugin-specific setting should also use the global setting
    allowed_stages = None

    try:
        allowed_stages = plugin.get_option('stage')
    except (AttributeError, KeyError):
        pass

    if allowed_stages:
        return allowed_stages in ('all', stage)

    # plugin didn't declare a preference; consult global config
    config_stage_override = C.RUN_VARS_PLUGINS
    if config_stage_override == 'demand' and stage == 'inventory':
        return False
    elif config_stage_override == 'start' and stage == 'task':
        return False
    return True


def get_vars_from_path(loader, path, entities, stage):

    data = {}

    if cached_vars_plugin_order is None:
        _load_vars_plugins_order()

    for plugin_name in cached_vars_plugin_order:
        if (plugin := vars_loader.get(plugin_name)) is None:
            continue

        if not _plugin_should_run(plugin, stage):
            continue

        if (new_vars := get_plugin_vars(loader, plugin, path, entities)) != {}:
            data = combine_vars(data, new_vars)

    return data


def get_vars_from_inventory_sources(loader, sources, entities, stage):

    data = {}
    for path in sources:

        if path is None:
            continue
        if ',' in path and not os.path.exists(path):  # skip host lists
            continue
        elif not os.path.isdir(path):
            # always pass the directory of the inventory source file
            path = os.path.dirname(path)

        if (new_vars := get_vars_from_path(loader, path, entities, stage)) != {}:
            data = combine_vars(data, new_vars)

    return data
