#!/usr/bin/python

# Copyright (c) 2013, Alexander Winkler <mail () winkler-alexander.de>
# based on svr4pkg by
#  Boyd Adamson <boyd () boydadamson.com> (2012)
#
# 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: pkgutil
short_description: OpenCSW package management on Solaris
description:
  - This module installs, updates and removes packages from the OpenCSW project for Solaris.
  - Unlike the M(community.general.svr4pkg) module, it resolves and downloads dependencies.
  - See U(https://www.opencsw.org/) for more information about the project.
author:
  - Alexander Winkler (@dermute)
  - David Ponessa (@scathatheworm)
extends_documentation_fragment:
  - community.general.attributes
attributes:
  check_mode:
    support: full
    details:
      - In order to check the availability of packages, the catalog cache under C(/var/opt/csw/pkgutil) may be refreshed even
        in check mode.
  diff_mode:
    support: none
options:
  name:
    description:
      - The name of the package.
      - When using O(state=latest), this can be V('*'), which updates all installed packages managed by pkgutil.
    type: list
    required: true
    elements: str
    aliases: [pkg]
  site:
    description:
      - The repository path to install the package from.
      - Its global definition is in C(/etc/opt/csw/pkgutil.conf).
    type: str
  state:
    description:
      - Whether to install (V(present)/V(installed)), or remove (V(absent)/V(removed)) packages.
      - The upgrade (V(latest)) operation updates/installs the packages to the latest version available.
    type: str
    required: true
    choices: [absent, installed, latest, present, removed]
  update_catalog:
    description:
      - If you always want to refresh your catalog from the mirror, even when it is not stale, set this to V(true).
    type: bool
    default: false
  force:
    description:
      - To allow the update process to downgrade packages to match what is present in the repository, set this to V(true).
      - This is useful for rolling back to stable from testing, or similar operations.
    type: bool
    default: false
    version_added: 1.2.0
"""

EXAMPLES = r"""
- name: Install a package
  community.general.pkgutil:
    name: CSWcommon
    state: present

- name: Install a package from a specific repository
  community.general.pkgutil:
    name: CSWnrpe
    site: ftp://myinternal.repo/opencsw/kiel
    state: latest

- name: Remove a package
  community.general.pkgutil:
    name: CSWtop
    state: absent

- name: Install several packages
  community.general.pkgutil:
    name:
      - CSWsudo
      - CSWtop
    state: present

- name: Update all packages
  community.general.pkgutil:
    name: '*'
    state: latest

- name: Update all packages and force versions to match latest in catalog
  community.general.pkgutil:
    name: '*'
    state: latest
    force: true
"""

RETURN = r""" # """

from ansible.module_utils.basic import AnsibleModule


def packages_not_installed(module, names):
    """Check if each package is installed and return list of the ones absent"""
    pkgs = []
    for pkg in names:
        rc, out, err = run_command(module, ["pkginfo", "-q", pkg])
        if rc != 0:
            pkgs.append(pkg)
    return pkgs


def packages_installed(module, names):
    """Check if each package is installed and return list of the ones present"""
    pkgs = []
    for pkg in names:
        if not pkg.startswith("CSW"):
            continue
        rc, out, err = run_command(module, ["pkginfo", "-q", pkg])
        if rc == 0:
            pkgs.append(pkg)
    return pkgs


def packages_not_latest(module, names, site, update_catalog):
    """Check status of each package and return list of the ones with an upgrade available"""
    cmd = ["pkgutil"]
    if update_catalog:
        cmd.append("-U")
    cmd.append("-c")
    if site is not None:
        cmd.extend(["-t", site])
    if names != ["*"]:
        cmd.extend(names)
    rc, out, err = run_command(module, cmd)

    # Find packages in the catalog which are not up to date
    packages = []
    for line in out.split("\n")[1:-1]:
        if "catalog" not in line and "SAME" not in line:
            packages.append(line.split(" ")[0])

    # Remove duplicates
    return list(set(packages))


def run_command(module, cmd, **kwargs):
    progname = cmd[0]
    cmd[0] = module.get_bin_path(progname, True, ["/opt/csw/bin"])
    return module.run_command(cmd, **kwargs)


def package_install(module, state, pkgs, site, update_catalog, force):
    cmd = ["pkgutil"]
    if module.check_mode:
        cmd.append("-n")
    cmd.append("-iy")
    if update_catalog:
        cmd.append("-U")
    if site is not None:
        cmd.extend(["-t", site])
    if force:
        cmd.append("-f")
    cmd.extend(pkgs)
    return run_command(module, cmd)


def package_upgrade(module, pkgs, site, update_catalog, force):
    cmd = ["pkgutil"]
    if module.check_mode:
        cmd.append("-n")
    cmd.append("-uy")
    if update_catalog:
        cmd.append("-U")
    if site is not None:
        cmd.extend(["-t", site])
    if force:
        cmd.append("-f")
    cmd += pkgs
    return run_command(module, cmd)


def package_uninstall(module, pkgs):
    cmd = ["pkgutil"]
    if module.check_mode:
        cmd.append("-n")
    cmd.append("-ry")
    cmd.extend(pkgs)
    return run_command(module, cmd)


def main():
    module = AnsibleModule(
        argument_spec=dict(
            name=dict(type="list", elements="str", required=True, aliases=["pkg"]),
            state=dict(type="str", required=True, choices=["absent", "installed", "latest", "present", "removed"]),
            site=dict(type="str"),
            update_catalog=dict(type="bool", default=False),
            force=dict(type="bool", default=False),
        ),
        supports_check_mode=True,
    )
    name = module.params["name"]
    state = module.params["state"]
    site = module.params["site"]
    update_catalog = module.params["update_catalog"]
    force = module.params["force"]

    rc = None
    out = ""
    err = ""
    result = dict(
        name=name,
        state=state,
    )

    if state in ["installed", "present"]:
        # Fail with an explicit error when trying to "install" '*'
        if name == ["*"]:
            module.fail_json(msg="Can not use 'state: present' with name: '*'")

        # Build list of packages that are actually not installed from the ones requested
        pkgs = packages_not_installed(module, name)

        # If the package list is empty then all packages are already present
        if pkgs == []:
            module.exit_json(changed=False)

        (rc, out, err) = package_install(module, state, pkgs, site, update_catalog, force)
        if rc != 0:
            module.fail_json(msg=(err or out))

    elif state in ["latest"]:
        # When using latest for *
        if name == ["*"]:
            # Check for packages that are actually outdated
            pkgs = packages_not_latest(module, name, site, update_catalog)

            # If the package list comes up empty, everything is already up to date
            if pkgs == []:
                module.exit_json(changed=False)

            # If there are packages to update, just empty the list and run the command without it
            # pkgutil logic is to update all when run without packages names
            pkgs = []
            (rc, out, err) = package_upgrade(module, pkgs, site, update_catalog, force)
            if rc != 0:
                module.fail_json(msg=(err or out))
        else:
            # Build list of packages that are either outdated or not installed
            pkgs = packages_not_installed(module, name)
            pkgs += packages_not_latest(module, name, site, update_catalog)

            # If the package list is empty that means all packages are installed and up to date
            if pkgs == []:
                module.exit_json(changed=False)

            (rc, out, err) = package_upgrade(module, pkgs, site, update_catalog, force)
            if rc != 0:
                module.fail_json(msg=(err or out))

    elif state in ["absent", "removed"]:
        # Build list of packages requested for removal that are actually present
        pkgs = packages_installed(module, name)

        # If the list is empty, no packages need to be removed
        if pkgs == []:
            module.exit_json(changed=False)

        (rc, out, err) = package_uninstall(module, pkgs)
        if rc != 0:
            module.fail_json(msg=(err or out))

    if rc is None:
        # pkgutil was not executed because the package was already present/absent/up to date
        result["changed"] = False
    elif rc == 0:
        result["changed"] = True
    else:
        result["changed"] = False
        result["failed"] = True

    if out:
        result["stdout"] = out
    if err:
        result["stderr"] = err

    module.exit_json(**result)


if __name__ == "__main__":
    main()
