# 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 absolute_import, division, print_function

__metaclass__ = type

DOCUMENTATION = '''
name: controller
author:
  - Matthew Jones (@matburt)
  - Yunfan Zhang (@YunfanZhang42)
short_description: Ansible dynamic inventory plugin for the Automation Platform Controller.
description:
    - Reads inventories from the Automation Platform Controller.
    - Supports reading configuration from both YAML config file and environment variables.
    - If reading from the YAML file, the file name must end with controller.(yml|yaml) or controller_inventory.(yml|yaml),
      the path in the command would be /path/to/controller_inventory.(yml|yaml). If some arguments in the config file
      are missing, this plugin will try to fill in missing arguments by reading from environment variables.
    - If reading configurations from environment variables, the path in the command must be @controller_inventory.
options:
    inventory_id:
        description:
            - The ID of the inventory that you wish to import.
            - This is allowed to be either the inventory primary key or its named URL slug.
            - Primary key values will be accepted as strings or integers, and URL slugs must be strings.
            - Named URL slugs follow the syntax of "inventory_name++organization_name".
        type: raw
        env:
            - name: CONTROLLER_INVENTORY
        required: True
    include_metadata:
        description: Make extra requests to provide all group vars with metadata about the source host.
        type: bool
        default: False
extends_documentation_fragment: awx.awx.auth_plugin
'''

EXAMPLES = '''
# Before you execute the following commands, you should make sure this file is in your plugin path,
# and you enabled this plugin.

# Example for using controller_inventory.yml file

plugin: awx.awx.controller
host: your_automation_controller_server_network_address
username: your_automation_controller_username
password: your_automation_controller_password
inventory_id: the_ID_of_targeted_automation_controller_inventory
# Then you can run the following command.
# If some of the arguments are missing, Ansible will attempt to read them from environment variables.
# ansible-inventory -i /path/to/controller_inventory.yml --list

# Example for reading from environment variables:

# Set environment variables:
# export CONTROLLER_HOST=YOUR_AUTOMATION_PLATFORM_CONTROLLER_HOST_ADDRESS
# export CONTROLLER_USERNAME=YOUR_CONTROLLER_USERNAME
# export CONTROLLER_PASSWORD=YOUR_CONTROLLER_PASSWORD
# export CONTROLLER_INVENTORY=THE_ID_OF_TARGETED_INVENTORY
# Read the inventory specified in CONTROLLER_INVENTORY from the controller, and list them.
# The inventory path must always be @controller_inventory if you are reading all settings from environment variables.
# ansible-inventory -i @controller_inventory --list
'''

import os

from ansible.module_utils import six
from ansible.module_utils._text import to_text, to_native
from ansible.errors import AnsibleParserError, AnsibleOptionsError
from ansible.plugins.inventory import BaseInventoryPlugin
from ansible.config.manager import ensure_type

from ansible.module_utils.six import raise_from
from ..module_utils.controller_api import ControllerAPIModule


def handle_error(**kwargs):
    raise AnsibleParserError(to_native(kwargs.get('msg')))


class InventoryModule(BaseInventoryPlugin):
    NAME = 'awx.awx.controller'  # REPLACE
    # Stays backward compatible with the inventory script.
    # If the user supplies '@controller_inventory' as path, the plugin will read from environment variables.
    no_config_file_supplied = False

    def verify_file(self, path):
        if path.endswith('@controller_inventory') or path.endswith('@tower_inventory'):
            self.no_config_file_supplied = True
            return True
        elif super().verify_file(path):
            return path.endswith(
                (
                    'controller_inventory.yml',
                    'controller_inventory.yaml',
                    'controller.yml',
                    'controller.yaml',
                    'tower_inventory.yml',
                    'tower_inventory.yaml',
                    'tower.yml',
                    'tower.yaml',
                )
            )
        else:
            return False

    def warn_callback(self, warning):
        self.display.warning(warning)

    def parse(self, inventory, loader, path, cache=True):
        super().parse(inventory, loader, path)
        if not self.no_config_file_supplied and os.path.isfile(path):
            self._read_config_data(path)

        # Defer processing of params to logic shared with the modules
        module_params = {}
        for plugin_param, module_param in ControllerAPIModule.short_params.items():
            opt_val = self.get_option(plugin_param)
            if opt_val is not None:
                module_params[module_param] = opt_val

        module = ControllerAPIModule(argument_spec={}, direct_params=module_params, error_callback=handle_error, warn_callback=self.warn_callback)

        # validate type of inventory_id because we allow two types as special case
        inventory_id = self.get_option('inventory_id')
        if isinstance(inventory_id, int):
            inventory_id = to_text(inventory_id, nonstring='simplerepr')
        else:
            try:
                inventory_id = ensure_type(inventory_id, 'str')
            except ValueError as e:
                raise_from(AnsibleOptionsError(
                    'Invalid type for configuration option inventory_id, ' 'not integer, and cannot convert to string: {err}'.format(err=to_native(e))
                ), e)
        inventory_id = inventory_id.replace('/', '')
        inventory_url = '/api/v2/inventories/{inv_id}/script/'.format(inv_id=inventory_id)

        inventory = module.get_endpoint(inventory_url, data={'hostvars': '1', 'towervars': '1', 'all': '1'})['json']

        # To start with, create all the groups.
        for group_name in inventory:
            if group_name != '_meta':
                self.inventory.add_group(group_name)

        # Then, create all hosts and add the host vars.
        all_hosts = inventory['_meta']['hostvars']
        for host_name, host_vars in six.iteritems(all_hosts):
            self.inventory.add_host(host_name)
            for var_name, var_value in six.iteritems(host_vars):
                self.inventory.set_variable(host_name, var_name, var_value)

        # Lastly, create to group-host and group-group relationships, and set group vars.
        for group_name, group_content in six.iteritems(inventory):
            if group_name != 'all' and group_name != '_meta':
                # First add hosts to groups
                for host_name in group_content.get('hosts', []):
                    self.inventory.add_host(host_name, group_name)
                # Then add the parent-children group relationships.
                for child_group_name in group_content.get('children', []):
                    # add the child group to groups, if its already there it will just throw a warning
                    self.inventory.add_group(child_group_name)
                    self.inventory.add_child(group_name, child_group_name)
            # Set the group vars. Note we should set group var for 'all', but not '_meta'.
            if group_name != '_meta':
                for var_name, var_value in six.iteritems(group_content.get('vars', {})):
                    self.inventory.set_variable(group_name, var_name, var_value)

        # Fetch extra variables if told to do so
        if self.get_option('include_metadata'):

            config_data = module.get_endpoint('/api/v2/config/')['json']

            server_data = {}
            server_data['license_type'] = config_data.get('license_info', {}).get('license_type', 'unknown')
            for key in ('version', 'ansible_version'):
                server_data[key] = config_data.get(key, 'unknown')
            self.inventory.set_variable('all', 'tower_metadata', server_data)
            self.inventory.set_variable('all', 'controller_metadata', server_data)

        # Clean up the inventory.
        self.inventory.reconcile_inventory()
