#!/usr/local/bin/python

# The MIT License (MIT)
#
# Copyright (c) 2014 Austin Hyde
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

def yay_in_path(module):
  rc, _, _ = module.run_command('which yay', check_rc=False)
  return rc == 0


def pacman_in_path(module):
  rc, _, _ = module.run_command('which pacman', check_rc=False)
  return rc == 0

def get_version(yay_output):
    '''Take yay -Qi or yay -Si output and get the Version'''
    lines = yay_output.split('\n')
    for line in lines:
        if 'Version' in line:
            return line.split(':')[1].strip()
    return None

def query_package(module, pkg, state):
  '''
  Query the package status in both the local system and the repository.
  Returns three booleans to indicate:
    * If the package is installed
    * If the package is up-to-date
    * Whether online information was available
  '''
  local_check_cmd = 'yay -Qi %s' % pkg
  local_check_rc, local_check_stdout, _ = module.run_command(local_check_cmd, check_rc=False)
  if local_check_rc != 0:
    return False, False, False

  # No need to check for the repo version in some situations
  # Indicate the package is out-of-date, because we chose not to check
  if state == 'present' or state == 'absent':
    return True, False, False

  local_version = get_version(local_check_stdout)

  repo_check_cmd = 'yay -Si %s' % pkg
  repo_check_rc, repo_check_stdout, repo_check_stderr = module.run_command(repo_check_cmd, check_rc=False)
  repo_version = get_version(repo_check_stdout)

  if repo_check_rc == 0 and repo_check_stderr == '':
    return True, (local_version == repo_version), False
  else:
    # Indicate package is up-to-date, but just because we hit an error contacting the repo
    return True, True, True

def update_package_db(module):
  rc, _, stderr = module.run_command('yay -Sy', check_rc=False)

  if rc == 0 and stderr == '':
    return False, 'Package DB up-to-date'
  elif rc == 1 and stderr == '':
    return True, 'Updated the package DB'
  else:
    module.fail_json(msg='could not update package db: %s' % stderr)

def upgrade(module):
  check_rc, check_stdout, check_stderr = module.run_command('yay -Qqu', check_rc=False)

  if check_rc == 0 and check_stderr == '' and module.check_mode:
    return True, '%s package(s) would be upgraded' % (len(check_stdout.split('\n')) - 1)
  elif check_rc == 0 and check_stderr == '' and not module.check_mode:
    upgrade_rc, _, upgrade_stderr = module.run_command(
      'yay -Su --noconfirm',
      check_rc=False,
    )

    if upgrade_rc == 0:
      return True, 'System upgraded'
    else:
      module.fail_json(msg='unable to upgrade: %s' % upgrade_stderr)
  elif check_rc == 1 and check_stderr == '':
    return False, 'Nothing to upgrade'
  else:
    module.fail_json(msg='unable to check for upgrade: %s' % check_stderr)

def get_sudo_user(module):
  # ansible sets the SUDO_USER environment variable.  Default to using this,
  # checking USER and then `logname` as backups.
  user = os.environ.get('SUDO_USER')

  # If ansible is run as root with become_user set, use the specified user
  # instead of root.
  if not user or user == 'root':
    user = os.environ.get('USER')

  if not user:
    rc, stdout, _ = module.run_command('logname', check_rc=True)
    user = stdout

  return user

def check_packages(module, pkgs, state):
  would_be_changed = []

  for pkg in pkgs:
    installed, updated, _ = query_package(module, pkg, state)
    if ((state in ['present', 'latest'] and not installed) or
        (state == 'latest' and not updated) or
        (state == 'absent' and installed)):
      would_be_changed.append(pkg)

  word = 'installed'
  if state == 'absent':
    word = 'removed'

  if would_be_changed:
    return True, '%s package(s) would be %s' % (len(would_be_changed), word)
  else:
    return False, 'All packages are already %s' % word

def install_packages(module, pkgs, state):
  num_installed = 0
  package_err = []
  message = ''

  sudo_user = get_sudo_user(module)
  cmd = 'sudo -u %s yay --noconfirm -S %s'

  for pkg in pkgs:
    installed, updated, latest_error = query_package(module, pkg, state)
    if latest_error and state == 'latest':
        package_err.append(pkg)

    if installed and (state == 'present' or (state == 'latest' and updated)):
        continue

    rc, _, stderr = module.run_command(cmd % (sudo_user, pkg), check_rc=False)

    if rc != 0:
      module.fail_json(msg='Failed to install package %s, because: %s' % (pkg, stderr))

    num_installed += 1

  if state == 'latest' and len(package_err) > 0:
    message = 'But could not ensure "latest" state for %s package(s) as remote version could not be fetched.' % package_err

  if num_installed > 0:
    return True, 'Installed %s package(s). %s' % (num_installed, message)
  else:
    return False, 'All packages were already installed. %s' % message

def remove_packages(module, pkgs, recurse, state):
  num_removed = 0

  arg = 'R'
  word = 'remove'
  if recurse:
    arg = 'Rs'
    word = 'recursively remove'

  cmd = 'pacman -%s --noconfirm %s'

  for pkg in pkgs:
    installed, _, _ = query_package(module, pkg, state)
    if not installed:
      continue

    rc, _, stderr = module.run_command(cmd % (arg, pkg), check_rc=False)

    if rc != 0:
      module.fail_json(msg='failed to %s package %s because: %s' % (word, pkg, stderr))

    num_removed += 1

  if num_removed > 0:
    return True, 'Removed %s package(s)' % num_removed
  else:
    return False, 'All packages were already removed'


def main():
  module = AnsibleModule(
    argument_spec = dict(
      name         = dict(type='list'),
      state        = dict(
          default='present',
          choices=['absent', 'present', 'latest'],
      ),
      recurse      = dict(default='no', type='bool'),
      upgrade      = dict(default='no', type='bool'),
      update_cache = dict(
          default='no',
          aliases=['update-cache'],
          type='bool',
      ),
    ),
    required_one_of = [['name', 'update_cache', 'upgrade']],
    supports_check_mode = True
  )

  if not yay_in_path(module):
    module.fail_json(msg="could not locate yay executable")

  if not pacman_in_path(module):
    module.fail_json(msg="could not locate pacman executable")

  p = module.params

  changed = False
  messages = []
  if p["update_cache"] and not module.check_mode:
    updated, update_message = update_package_db(module)
    changed = changed or updated
    messages.append(update_message)

  if p['update_cache'] and module.check_mode:
    changed = True
    messages.append('Would have updated the package cache')

  if p['upgrade']:
     upgraded, upgrade_message = upgrade(module)
     changed = changed or upgraded
     messages.append(upgrade_message)

  if p['name'] and module.check_mode:
    packages_would_change, check_message = check_packages(
        module,
        p['name'],
        p['state'],
    )
    changed = changed or packages_would_change
    messages.append(check_message)
  elif p['name'] and not module.check_mode:
    if p['name']:
      if p['state'] in ['present', 'latest']:
        packages_changed, package_message = install_packages(
            module,
            p['name'],
            p['state'],
        )
      elif p['state'] == 'absent':
        packages_changed, package_message = remove_packages(
            module,
            p['name'],
            p['recurse'],
            p['state'],
        )

      changed = changed or packages_changed
      messages.append(package_message)

  module.exit_json(changed=changed, msg='. '.join(messages))


from ansible.module_utils.basic import *
main()
