#!/usr/bin/python

# Copyright (c) 2013-2014, Christian Berendt <berendt@b1-systems.de>
# 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: apache2_module
author:
  - Christian Berendt (@berendt)
  - Ralf Hertel (@n0trax)
  - Robin Roth (@robinro)
short_description: Enables/disables a module of the Apache2 webserver
description:
  - Enables or disables a specified module of the Apache2 webserver.
extends_documentation_fragment:
  - community.general._attributes
attributes:
  check_mode:
    support: full
  diff_mode:
    support: none
options:
  name:
    type: str
    description:
      - Name of the module to enable/disable as given to C(a2enmod)/C(a2dismod).
    required: true
  identifier:
    type: str
    description:
      - Identifier of the module as listed by C(apache2ctl -M). This is optional and usually determined automatically by the
        common convention of appending V(_module) to O(name) as well as custom exception for popular modules.
  force:
    description:
      - Force disabling of default modules and override Debian warnings.
    type: bool
    default: false
  state:
    type: str
    description:
      - Desired state of the module.
    choices: ['present', 'absent']
    default: present
  ignore_configcheck:
    description:
      - Ignore configuration checks about inconsistent module configuration. Especially for mpm_* modules.
    type: bool
    default: false
  warn_mpm_absent:
    description:
      - Control the behavior of the warning process for MPM modules.
    type: bool
    default: true
    version_added: 6.3.0
requirements: ["a2enmod", "a2dismod"]
notes:
  - This does not work on RedHat-based distributions. It does work on Debian- and SuSE-based distributions. Whether it works
    on others depend on whether the C(a2enmod) and C(a2dismod) tools are available or not.
"""

EXAMPLES = r"""
- name: Enable the Apache2 module wsgi
  community.general.apache2_module:
    state: present
    name: wsgi

- name: Disables the Apache2 module wsgi
  community.general.apache2_module:
    state: absent
    name: wsgi

- name: Disable default modules for Debian
  community.general.apache2_module:
    state: absent
    name: autoindex
    force: true

- name: Disable mpm_worker and ignore warnings about missing mpm module
  community.general.apache2_module:
    state: absent
    name: mpm_worker
    ignore_configcheck: true

- name: Disable mpm_event, enable mpm_prefork and ignore warnings about missing mpm module
  community.general.apache2_module:
    name: "{{ item.module }}"
    state: "{{ item.state }}"
    warn_mpm_absent: false
    ignore_configcheck: true
  loop:
    - module: mpm_event
      state: absent
    - module: mpm_prefork
      state: present

- name: Enable dump_io module, which is identified as dumpio_module inside apache2
  community.general.apache2_module:
    state: present
    name: dump_io
    identifier: dumpio_module
"""

RETURN = r"""
result:
  description: Message about action taken.
  returned: always
  type: str
"""

import re

# import module snippets
from ansible.module_utils.basic import AnsibleModule

_re_threaded = re.compile(r"threaded: *yes")


def _run_threaded(module):
    control_binary = _get_ctl_binary(module)
    result, stdout, stderr = module.run_command([control_binary, "-V"])

    return bool(_re_threaded.search(stdout))


def _get_ctl_binary(module):
    for command in ["apache2ctl", "apachectl"]:
        ctl_binary = module.get_bin_path(command)
        if ctl_binary is not None:
            return ctl_binary

    module.fail_json(msg="Neither of apache2ctl nor apachectl found. At least one apache control binary is necessary.")


def _module_is_enabled(module):
    control_binary = _get_ctl_binary(module)
    result, stdout, stderr = module.run_command([control_binary, "-M"])

    if result != 0:
        error_msg = f"Error executing {control_binary}: {stderr}"
        if module.params["ignore_configcheck"]:
            if "AH00534" in stderr and "mpm_" in module.params["name"]:
                if module.params["warn_mpm_absent"]:
                    module.warn(
                        "No MPM module loaded! apache2 reload AND other module actions"
                        " will fail if no MPM module is loaded immediately."
                    )
            else:
                module.warn(error_msg)
            return False
        else:
            module.fail_json(msg=error_msg)

    searchstring = f" {module.params['identifier']}"
    return searchstring in stdout


def create_apache_identifier(name):
    """
    By convention if a module is loaded via name, it appears in apache2ctl -M as
    name_module.

    Some modules don't follow this convention and we use replacements for those."""

    # a2enmod name replacement to apache2ctl -M names
    text_workarounds = [
        ("shib", "mod_shib"),
        ("shib2", "mod_shib"),
        ("evasive", "evasive20_module"),
    ]

    # re expressions to extract subparts of names
    re_workarounds = [
        ("php8", re.compile(r"^(php)[\d\.]+")),
        ("php", re.compile(r"^(php\d)\.")),
    ]

    for a2enmod_spelling, module_name in text_workarounds:
        if a2enmod_spelling in name:
            return module_name

    for search, reexpr in re_workarounds:
        if search in name:
            try:
                rematch = reexpr.search(name)
                return f"{rematch.group(1)}_module"
            except AttributeError:
                pass

    return f"{name}_module"


def _set_state(module, state):
    name = module.params["name"]
    force = module.params["force"]

    want_enabled = state == "present"
    state_string = {"present": "enabled", "absent": "disabled"}[state]
    a2mod_binary = {"present": "a2enmod", "absent": "a2dismod"}[state]
    success_msg = f"Module {name} {state_string}"

    if _module_is_enabled(module) != want_enabled:
        if module.check_mode:
            module.exit_json(changed=True, result=success_msg)

        a2mod_binary_path = module.get_bin_path(a2mod_binary)
        if a2mod_binary_path is None:
            module.fail_json(
                msg=f"{a2mod_binary} not found. Perhaps this system does not use {a2mod_binary} to manage apache"
            )

        a2mod_binary_cmd = [a2mod_binary_path]

        if not want_enabled and force:
            # force exists only for a2dismod on debian
            a2mod_binary_cmd.append("-f")

        result, stdout, stderr = module.run_command(a2mod_binary_cmd + [name])

        if _module_is_enabled(module) == want_enabled:
            module.exit_json(changed=True, result=success_msg)
        else:
            msg = (
                f"Failed to set module {name} to {state_string}:\n"
                f"{stdout}\n"
                f"Maybe the module identifier ({module.params['identifier']}) was guessed incorrectly."
                'Consider setting the "identifier" option.'
            )
            module.fail_json(msg=msg, rc=result, stdout=stdout, stderr=stderr)
    else:
        module.exit_json(changed=False, result=success_msg)


def main():
    module = AnsibleModule(
        argument_spec=dict(
            name=dict(required=True),
            identifier=dict(type="str"),
            force=dict(type="bool", default=False),
            state=dict(default="present", choices=["absent", "present"]),
            ignore_configcheck=dict(type="bool", default=False),
            warn_mpm_absent=dict(type="bool", default=True),
        ),
        supports_check_mode=True,
    )
    module.run_command_environ_update = {"LANGUAGE": "C", "LC_ALL": "C"}

    name = module.params["name"]
    if name == "cgi" and module.params["state"] == "present" and _run_threaded(module):
        module.fail_json(msg="Your MPM seems to be threaded, therefore enabling cgi module is not allowed.")

    if not module.params["identifier"]:
        module.params["identifier"] = create_apache_identifier(module.params["name"])

    if module.params["state"] in ["present", "absent"]:
        _set_state(module, module.params["state"])


if __name__ == "__main__":
    main()
