#!/usr/bin/python
# Copyright (c) 2015, Mathew Davies <thepixeldeveloper@googlemail.com>
# Copyright (c) 2017, Sam Doran <sdoran@redhat.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations

DOCUMENTATION = r"""
module: elasticsearch_plugin
short_description: Manage Elasticsearch plugins
description:
  - Manages Elasticsearch plugins.
author:
  - Mathew Davies (@ThePixelDeveloper)
  - Sam Doran (@samdoran)
extends_documentation_fragment:
  - community.general._attributes
attributes:
  check_mode:
    support: full
  diff_mode:
    support: none
options:
  name:
    description:
      - Name of the plugin to install.
    required: true
    type: str
  state:
    description:
      - Desired state of a plugin.
    choices: ["present", "absent"]
    default: present
    type: str
  src:
    description:
      - Optionally set the source location to retrieve the plugin from. This can be a C(file://) URL to install from a local
        file, or a remote URL. If this is not set, the plugin location is just based on the name.
      - The name parameter must match the descriptor in the plugin ZIP specified.
      - Is only used if the state would change, which is solely checked based on the name parameter. If, for example, the
        plugin is already installed, changing this has no effect.
      - For ES 1.x use O(url).
    type: str
  url:
    description:
      - Set exact URL to download the plugin from (Only works for ES 1.x).
      - For ES 2.x and higher, use src.
    type: str
  timeout:
    description:
      - 'Timeout setting: V(30s), V(1m), V(1h)...'
      - Only valid for Elasticsearch < 5.0. This option is ignored for Elasticsearch > 5.0.
    default: 1m
    type: str
  force:
    description:
      - Force batch mode when installing plugins. This is only necessary if a plugin requires additional permissions and console
        detection fails.
    default: false
    type: bool
  plugin_bin:
    description:
      - Location of the plugin binary. If this file is not found, the default plugin binaries are used.
    type: path
  plugin_dir:
    description:
      - Your configured plugin directory specified in Elasticsearch.
    default: /usr/share/elasticsearch/plugins/
    type: path
  proxy_host:
    description:
      - Proxy host to use during plugin installation.
    type: str
  proxy_port:
    description:
      - Proxy port to use during plugin installation.
    type: str
  version:
    description:
      - Version of the plugin to be installed. If plugin exists with previous version, it is NOT updated.
    type: str
"""

EXAMPLES = r"""
- name: Install Elasticsearch Head plugin in Elasticsearch 2.x
  community.general.elasticsearch_plugin:
    name: mobz/elasticsearch-head
    state: present

- name: Install a specific version of Elasticsearch Head in Elasticsearch 2.x
  community.general.elasticsearch_plugin:
    name: mobz/elasticsearch-head
    version: 2.0.0

- name: Uninstall Elasticsearch head plugin in Elasticsearch 2.x
  community.general.elasticsearch_plugin:
    name: mobz/elasticsearch-head
    state: absent

- name: Install a specific plugin in Elasticsearch >= 5.0
  community.general.elasticsearch_plugin:
    name: analysis-icu
    state: present

- name: Install the ingest-geoip plugin with a forced installation
  community.general.elasticsearch_plugin:
    name: ingest-geoip
    state: present
    force: true
"""

import os

from ansible.module_utils.basic import AnsibleModule

PACKAGE_STATE_MAP = dict(present="install", absent="remove")

PLUGIN_BIN_PATHS = tuple(["/usr/share/elasticsearch/bin/elasticsearch-plugin", "/usr/share/elasticsearch/bin/plugin"])


def parse_plugin_repo(string):
    elements = string.split("/")

    # We first consider the simplest form: pluginname
    repo = elements[0]

    # We consider the form: username/pluginname
    if len(elements) > 1:
        repo = elements[1]

    # remove elasticsearch- prefix
    # remove es- prefix
    for string in ("elasticsearch-", "es-"):
        if repo.startswith(string):
            return repo[len(string) :]

    return repo


def is_plugin_present(plugin_name, plugin_dir):
    return os.path.isdir(os.path.join(plugin_dir, plugin_name))


def parse_error(string):
    reason = "ERROR: "
    try:
        return string[string.index(reason) + len(reason) :].strip()
    except ValueError:
        return string


def install_plugin(module, plugin_bin, plugin_name, version, src, url, proxy_host, proxy_port, timeout, force):
    cmd = [plugin_bin, PACKAGE_STATE_MAP["present"]]
    is_old_command = os.path.basename(plugin_bin) == "plugin"

    # Timeout and version are only valid for plugin, not elasticsearch-plugin
    if is_old_command:
        if timeout:
            cmd.append("--timeout")
            cmd.append(timeout)

        if version:
            plugin_name = f"{plugin_name}/{version}"
            cmd[2] = plugin_name

    if proxy_host and proxy_port:
        java_opts = [
            f"-Dhttp.proxyHost={proxy_host}",
            f"-Dhttp.proxyPort={proxy_port}",
            f"-Dhttps.proxyHost={proxy_host}",
            f"-Dhttps.proxyPort={proxy_port}",
        ]
        module.run_command_environ_update = dict(
            CLI_JAVA_OPTS=" ".join(java_opts),  # Elasticsearch 8.x
            ES_JAVA_OPTS=" ".join(java_opts),
        )  # Older Elasticsearch versions

    # Legacy ES 1.x
    if url:
        cmd.append("--url")
        cmd.append(url)

    if force:
        cmd.append("--batch")
    if src:
        cmd.append(src)
    else:
        cmd.append(plugin_name)

    if module.check_mode:
        rc, out, err = 0, "check mode", ""
    else:
        rc, out, err = module.run_command(cmd)

    if rc != 0:
        reason = parse_error(out)
        module.fail_json(msg=f"Installing plugin '{plugin_name}' failed: {reason}", err=err)

    return True, cmd, out, err


def remove_plugin(module, plugin_bin, plugin_name):
    cmd = [plugin_bin, PACKAGE_STATE_MAP["absent"], parse_plugin_repo(plugin_name)]

    if module.check_mode:
        rc, out, err = 0, "check mode", ""
    else:
        rc, out, err = module.run_command(cmd)

    if rc != 0:
        reason = parse_error(out)
        module.fail_json(msg=f"Removing plugin '{plugin_name}' failed: {reason}", err=err)

    return True, cmd, out, err


def get_plugin_bin(module, plugin_bin=None):
    # Use the plugin_bin that was supplied first before trying other options
    valid_plugin_bin = None
    if plugin_bin and os.path.isfile(plugin_bin):
        valid_plugin_bin = plugin_bin

    else:
        # Add the plugin_bin passed into the module to the top of the list of paths to test,
        # testing for that binary name first before falling back to the default paths.
        bin_paths = list(PLUGIN_BIN_PATHS)
        if plugin_bin and plugin_bin not in bin_paths:
            bin_paths.insert(0, plugin_bin)

        # Get separate lists of dirs and binary names from the full paths to the
        # plugin binaries.
        plugin_dirs = list({os.path.dirname(x) for x in bin_paths})
        plugin_bins = list({os.path.basename(x) for x in bin_paths})

        # Check for the binary names in the default system paths as well as the path
        # specified in the module arguments.
        for bin_file in plugin_bins:
            valid_plugin_bin = module.get_bin_path(bin_file, opt_dirs=plugin_dirs)
            if valid_plugin_bin:
                break

    if not valid_plugin_bin:
        module.fail_json(
            msg=f"{plugin_bin} does not exist and no other valid plugin installers were found. Make sure Elasticsearch is installed."
        )

    return valid_plugin_bin


def main():
    module = AnsibleModule(
        argument_spec=dict(
            name=dict(required=True),
            state=dict(default="present", choices=list(PACKAGE_STATE_MAP.keys())),
            src=dict(),
            url=dict(),
            timeout=dict(default="1m"),
            force=dict(type="bool", default=False),
            plugin_bin=dict(type="path"),
            plugin_dir=dict(default="/usr/share/elasticsearch/plugins/", type="path"),
            proxy_host=dict(),
            proxy_port=dict(),
            version=dict(),
        ),
        mutually_exclusive=[("src", "url")],
        supports_check_mode=True,
    )

    name = module.params["name"]
    state = module.params["state"]
    url = module.params["url"]
    src = module.params["src"]
    timeout = module.params["timeout"]
    force = module.params["force"]
    plugin_bin = module.params["plugin_bin"]
    plugin_dir = module.params["plugin_dir"]
    proxy_host = module.params["proxy_host"]
    proxy_port = module.params["proxy_port"]
    version = module.params["version"]

    # Search provided path and system paths for valid binary
    plugin_bin = get_plugin_bin(module, plugin_bin)

    repo = parse_plugin_repo(name)
    present = is_plugin_present(repo, plugin_dir)

    # skip if the state is correct
    if (present and state == "present") or (state == "absent" and not present):
        module.exit_json(changed=False, name=name, state=state)

    if state == "present":
        changed, cmd, out, err = install_plugin(
            module, plugin_bin, name, version, src, url, proxy_host, proxy_port, timeout, force
        )

    elif state == "absent":
        changed, cmd, out, err = remove_plugin(module, plugin_bin, name)

    module.exit_json(changed=changed, cmd=cmd, name=name, state=state, url=url, timeout=timeout, stdout=out, stderr=err)


if __name__ == "__main__":
    main()
