#!/usr/bin/python
# Copyright (c) 2017, Ansible Project
# 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: ipa_host
author: Thomas Krahn (@Nosmoht)
short_description: Manage FreeIPA host
description:
  - Add, modify and delete an IPA host using IPA API.
attributes:
  check_mode:
    support: full
  diff_mode:
    support: none
options:
  fqdn:
    description:
      - Full qualified domain name.
      - Can not be changed as it is the unique identifier.
    required: true
    aliases: ["name"]
    type: str
  description:
    description:
      - A description of this host.
    type: str
  userclass:
    description:
      - Host category (semantics placed on this attribute are for local interpretation).
    type: str
    version_added: 12.0.0
  force:
    description:
      - Force host name even if not in DNS.
    type: bool
  ip_address:
    description:
      - Add the host to DNS with this IP address.
    type: str
  mac_address:
    description:
      - List of Hardware MAC address(es) off this host.
      - If option is omitted MAC addresses are not checked nor changed.
      - If an empty list is passed all assigned MAC addresses are removed.
      - MAC addresses that are already assigned but not passed are removed.
    aliases: ["macaddress"]
    type: list
    elements: str
  l:
    description:
      - Host locality (for example V(Baltimore, MD)).
    aliases: ["locality"]
    type: str
    version_added: 12.0.0
  ns_host_location:
    description:
      - Host location (for example V(Lab 2)).
    aliases: ["nshostlocation"]
    type: str
  ns_hardware_platform:
    description:
      - Host hardware platform (for example V(Lenovo T61")).
    aliases: ["nshardwareplatform"]
    type: str
  ns_os_version:
    description:
      - Host operating system and version (for example V(Fedora 9)).
    aliases: ["nsosversion"]
    type: str
  user_certificate:
    description:
      - List of Base-64 encoded server certificates.
      - If option is omitted certificates are not checked nor changed.
      - If an empty list is passed all assigned certificates are removed.
      - Certificates already assigned but not passed are removed.
    aliases: ["usercertificate"]
    type: list
    elements: str
  state:
    description:
      - State to ensure.
    default: present
    choices: ["absent", "disabled", "enabled", "present"]
    type: str
  force_creation:
    description:
      - Create host if O(state=disabled) or O(state=enabled) but not present.
    default: true
    type: bool
    version_added: 9.5.0
  update_dns:
    description:
      - If set V(true) with O(state=absent), then removes DNS records of the host managed by FreeIPA DNS.
      - This option has no effect for states other than V(absent).
    type: bool
  random_password:
    description: Generate a random password to be used in bulk enrollment.
    type: bool
extends_documentation_fragment:
  - community.general._ipa.documentation
  - community.general._ipa.connection_notes
  - community.general._attributes
"""

EXAMPLES = r"""
- name: Ensure host is present
  community.general.ipa_host:
    name: host01.example.com
    description: Example host
    userclass: Server
    ip_address: 192.168.0.123
    locality: Baltimore, MD
    ns_host_location: Lab
    ns_os_version: CentOS 7
    ns_hardware_platform: Lenovo T61
    mac_address:
      - "08:00:27:E3:B1:2D"
      - "52:54:00:BD:97:1E"
    state: present
    ipa_host: ipa.example.com
    ipa_user: admin
    ipa_pass: topsecret

- name: Generate a random password for bulk enrolment
  community.general.ipa_host:
    name: host01.example.com
    description: Example host
    ip_address: 192.168.0.123
    state: present
    ipa_host: ipa.example.com
    ipa_user: admin
    ipa_pass: topsecret
    random_password: true

- name: Ensure host is disabled
  community.general.ipa_host:
    name: host01.example.com
    state: disabled
    ipa_host: ipa.example.com
    ipa_user: admin
    ipa_pass: topsecret

- name: Ensure that all user certificates are removed
  community.general.ipa_host:
    name: host01.example.com
    user_certificate: []
    ipa_host: ipa.example.com
    ipa_user: admin
    ipa_pass: topsecret

- name: Ensure host is absent
  community.general.ipa_host:
    name: host01.example.com
    state: absent
    ipa_host: ipa.example.com
    ipa_user: admin
    ipa_pass: topsecret

- name: Ensure host and its DNS record is absent
  community.general.ipa_host:
    name: host01.example.com
    state: absent
    ipa_host: ipa.example.com
    ipa_user: admin
    ipa_pass: topsecret
    update_dns: true
"""

RETURN = r"""
host:
  description: Host as returned by IPA API.
  returned: always
  type: dict
host_diff:
  description: List of options that differ and would be changed.
  returned: if check mode and a difference is found
  type: list
"""

import traceback

from ansible.module_utils.basic import AnsibleModule

from ansible_collections.community.general.plugins.module_utils._ipa import IPAClient, ipa_argument_spec


class HostIPAClient(IPAClient):
    def __init__(self, module, host, port, protocol):
        super().__init__(module, host, port, protocol)

    def host_show(self, name):
        return self._post_json(method="host_show", name=name)

    def host_find(self, name):
        return self._post_json(method="host_find", name=None, item={"all": True, "fqdn": name})

    def host_add(self, name, host):
        return self._post_json(method="host_add", name=name, item=host)

    def host_mod(self, name, host):
        return self._post_json(method="host_mod", name=name, item=host)

    def host_del(self, name, update_dns):
        return self._post_json(method="host_del", name=name, item={"updatedns": update_dns})

    def host_disable(self, name):
        return self._post_json(method="host_disable", name=name)


def get_host_dict(
    description=None,
    userclass=None,
    force=None,
    ip_address=None,
    l=None,
    ns_host_location=None,
    ns_hardware_platform=None,
    ns_os_version=None,
    user_certificate=None,
    mac_address=None,
    random_password=None,
):
    data = {}
    if description is not None:
        data["description"] = description
    if userclass is not None:
        data["userclass"] = userclass
    if force is not None:
        data["force"] = force
    if ip_address is not None:
        data["ip_address"] = ip_address
    if l is not None:
        data["l"] = l
    if ns_host_location is not None:
        data["nshostlocation"] = ns_host_location
    if ns_hardware_platform is not None:
        data["nshardwareplatform"] = ns_hardware_platform
    if ns_os_version is not None:
        data["nsosversion"] = ns_os_version
    if user_certificate is not None:
        data["usercertificate"] = [{"__base64__": item} for item in user_certificate]
    if mac_address is not None:
        data["macaddress"] = mac_address
    if random_password is not None:
        data["random"] = random_password
    return data


def get_host_diff(client, ipa_host, module_host):
    non_updateable_keys = ["force", "ip_address"]
    if not module_host.get("random"):
        non_updateable_keys.append("random")
    for key in non_updateable_keys:
        if key in module_host:
            del module_host[key]

    return client.get_diff(ipa_data=ipa_host, module_data=module_host)


def ensure(module, client):
    name = module.params["fqdn"]
    state = module.params["state"]
    force_creation = module.params["force_creation"]

    ipa_host = client.host_find(name=name)
    module_host = get_host_dict(
        description=module.params["description"],
        userclass=module.params["userclass"],
        force=module.params["force"],
        ip_address=module.params["ip_address"],
        l=module.params["l"],
        ns_host_location=module.params["ns_host_location"],
        ns_hardware_platform=module.params["ns_hardware_platform"],
        ns_os_version=module.params["ns_os_version"],
        user_certificate=module.params["user_certificate"],
        mac_address=module.params["mac_address"],
        random_password=module.params["random_password"],
    )
    changed = False
    if state in ["present", "enabled", "disabled"]:
        if not ipa_host and (force_creation or state == "present"):
            changed = True
            if not module.check_mode:
                # OTP password generated by FreeIPA is visible only for host_add command
                # so, return directly from here.
                return changed, client.host_add(name=name, host=module_host)
        else:
            if not ipa_host and state in ["disabled", "enabled"]:
                module.fail_json(msg=f"No host with name {name} found")

            diff = get_host_diff(client, ipa_host, module_host)
            ipa_host_show = client.host_show(name=name)
            host_needs_to_be_disabled = ipa_host_show.get("has_keytab", True) and (
                module.params["random_password"] or state == "disabled"
            )
            if diff or host_needs_to_be_disabled:
                changed = True
                if not module.check_mode:
                    data = {}
                    for key in diff:
                        data[key] = module_host.get(key)
                    if "usercertificate" not in data:
                        data["usercertificate"] = [cert["__base64__"] for cert in ipa_host.get("usercertificate", [])]
                    if host_needs_to_be_disabled:
                        client.host_disable(name=name)
                    if diff:
                        return changed, client.host_mod(name=name, host=data)
                    else:
                        return changed, client.host_find(name=name)
    elif state == "absent":
        if ipa_host:
            changed = True
            update_dns = module.params.get("update_dns", False)
            if not module.check_mode:
                client.host_del(name=name, update_dns=update_dns)

    return changed, client.host_find(name=name)


def main():
    argument_spec = ipa_argument_spec()
    argument_spec.update(
        description=dict(type="str"),
        fqdn=dict(type="str", required=True, aliases=["name"]),
        force=dict(type="bool"),
        ip_address=dict(type="str"),
        l=dict(type="str", aliases=["locality"]),
        ns_host_location=dict(type="str", aliases=["nshostlocation"]),
        ns_hardware_platform=dict(type="str", aliases=["nshardwareplatform"]),
        ns_os_version=dict(type="str", aliases=["nsosversion"]),
        userclass=dict(type="str"),
        user_certificate=dict(type="list", aliases=["usercertificate"], elements="str"),
        mac_address=dict(type="list", aliases=["macaddress"], elements="str"),
        update_dns=dict(type="bool"),
        state=dict(type="str", default="present", choices=["present", "absent", "enabled", "disabled"]),
        random_password=dict(type="bool", no_log=False),
        force_creation=dict(type="bool", default=True),
    )

    module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)

    client = HostIPAClient(
        module=module,
        host=module.params["ipa_host"],
        port=module.params["ipa_port"],
        protocol=module.params["ipa_prot"],
    )

    try:
        client.login(username=module.params["ipa_user"], password=module.params["ipa_pass"])
        changed, host = ensure(module, client)
        module.exit_json(changed=changed, host=host)
    except Exception as e:
        module.fail_json(msg=f"{e}", exception=traceback.format_exc())


if __name__ == "__main__":
    main()
