#!/usr/bin/python

# Copyright (c) 2013, berenddeboer
# Written by berenddeboer <berend@pobox.com>
# Based on pkgng module written by bleader <bleader at ratonland.org>
#
# 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: portinstall
short_description: Installing packages from FreeBSD's ports system
description:
  - Manage packages for FreeBSD using C(portinstall).
extends_documentation_fragment:
  - community.general.attributes
attributes:
  check_mode:
    support: none
  diff_mode:
    support: none
options:
  name:
    description:
      - Name of package to install/remove.
    aliases: [pkg]
    required: true
    type: str
  state:
    description:
      - State of the package.
    choices: ['present', 'absent']
    default: present
    type: str
  use_packages:
    description:
      - Use packages instead of ports whenever available.
    type: bool
    default: true
author: "berenddeboer (@berenddeboer)"
"""

EXAMPLES = r"""
- name: Install package foo
  community.general.portinstall:
    name: foo
    state: present

- name: Install package security/cyrus-sasl2-saslauthd
  community.general.portinstall:
    name: security/cyrus-sasl2-saslauthd
    state: present

- name: Remove packages foo and bar
  community.general.portinstall:
    name: foo,bar
    state: absent
"""

import re

from shlex import quote as shlex_quote
from ansible.module_utils.basic import AnsibleModule


def query_package(module, name):
    pkg_info_path = module.get_bin_path("pkg_info", False)

    # Assume that if we have pkg_info, we haven't upgraded to pkgng
    if pkg_info_path:
        module.get_bin_path("pkg_glob", True)
        # TODO: convert run_comand() argument to list!
        rc, out, err = module.run_command(f"{pkg_info_path} -e `pkg_glob {shlex_quote(name)}`", use_unsafe_shell=True)
        pkg_info_path = [pkg_info_path]
    else:
        pkg_info_path = [module.get_bin_path("pkg", True), "info"]
        rc, out, err = module.run_command(pkg_info_path + [name])

    found = rc == 0

    if not found:
        # databases/mysql55-client installs as mysql-client, so try solving
        # that the ugly way. Pity FreeBSD doesn't have a fool proof way of checking
        # some package is installed
        name_without_digits = re.sub("[0-9]", "", name)
        if name != name_without_digits:
            rc, out, err = module.run_command(pkg_info_path + [name_without_digits])

        found = rc == 0

    return found


def matching_packages(module, name):
    ports_glob_path = module.get_bin_path("ports_glob", True)
    rc, out, err = module.run_command([ports_glob_path, name])
    # counts the number of packages found
    occurrences = out.count("\n")
    if occurrences == 0:
        name_without_digits = re.sub("[0-9]", "", name)
        if name != name_without_digits:
            rc, out, err = module.run_command([ports_glob_path, name_without_digits])
            occurrences = out.count("\n")
    return occurrences


def remove_packages(module, packages):
    remove_c = 0
    pkg_glob_path = module.get_bin_path("pkg_glob", True)

    # If pkg_delete not found, we assume pkgng
    pkg_delete_path = module.get_bin_path("pkg_delete", False)
    if not pkg_delete_path:
        pkg_delete_path = module.get_bin_path("pkg", True)
        pkg_delete_path = f"{pkg_delete_path} delete -y"

    # Using a for loop in case of error, we can report the package that failed
    for package in packages:
        # Query the package first, to see if we even need to remove
        if not query_package(module, package):
            continue

        # TODO: convert run_comand() argument to list!
        rc, out, err = module.run_command(
            f"{pkg_delete_path} `{pkg_glob_path} {shlex_quote(package)}`", use_unsafe_shell=True
        )

        if query_package(module, package):
            name_without_digits = re.sub("[0-9]", "", package)
            # TODO: convert run_comand() argument to list!
            rc, out, err = module.run_command(
                f"{pkg_delete_path} `{pkg_glob_path} {shlex_quote(name_without_digits)}`", use_unsafe_shell=True
            )
            if query_package(module, package):
                module.fail_json(msg=f"failed to remove {package}: {out}")

        remove_c += 1

    if remove_c > 0:
        module.exit_json(changed=True, msg=f"removed {remove_c} package(s)")

    module.exit_json(changed=False, msg="package(s) already absent")


def install_packages(module, packages, use_packages):
    install_c = 0

    # If portinstall not found, automagically install
    portinstall_path = module.get_bin_path("portinstall", False)
    if not portinstall_path:
        pkg_path = module.get_bin_path("pkg", False)
        if pkg_path:
            module.run_command([pkg_path, "install", "-y", "portupgrade"])
        portinstall_path = module.get_bin_path("portinstall", True)

    if use_packages:
        portinstall_params = ["--use-packages"]
    else:
        portinstall_params = []

    for package in packages:
        if query_package(module, package):
            continue

        # TODO: check how many match
        matches = matching_packages(module, package)
        if matches == 1:
            rc, out, err = module.run_command([portinstall_path, "--batch"] + portinstall_params + [package])
            if not query_package(module, package):
                module.fail_json(msg=f"failed to install {package}: {out}")
        elif matches == 0:
            module.fail_json(msg=f"no matches for package {package}")
        else:
            module.fail_json(msg=f"{matches} matches found for package name {package}")

        install_c += 1

    if install_c > 0:
        module.exit_json(changed=True, msg=f"present {install_c} package(s)")

    module.exit_json(changed=False, msg="package(s) already present")


def main():
    module = AnsibleModule(
        argument_spec=dict(
            state=dict(default="present", choices=["present", "absent"]),
            name=dict(aliases=["pkg"], required=True),
            use_packages=dict(type="bool", default=True),
        )
    )

    p = module.params

    pkgs = p["name"].split(",")

    if p["state"] == "present":
        install_packages(module, pkgs, p["use_packages"])

    elif p["state"] == "absent":
        remove_packages(module, pkgs)


if __name__ == "__main__":
    main()
