#!/usr/bin/env python3
# -*- coding: utf8 -*-

"""
This generates scripts that can be used for topological reinstallation of
packages that depend on a given set of packages. It will determine the full
dependency chain and create lists of the packages affected along with scripts
to reinstall the explicitly installed packages.

This script does not make any changes to the system. You can safely run it and
then inspect the generated lists and files before deciding to proceed.

The generated scripts are:

pkglist.txt
: A list all of the packages that would be affected, grouped by installation
reason.

reinstall.sh
: A script that will do a cascading removal of the target packages (ghc in this
example) and then attempt to reinstall the explicitly installed packages. It
assumes that those packages are available in the repos. If not, the user will
have to sort it out, for example by changing `pacman` to an AUR helper, or
removing the missing packages from the command.

reinstall_from_cache.sh
: Like `reinstall.sh`, this will also do a cascading removal, but instead of
reinstalling packages from the repos, this will attempt to reinstall packages
from the cache. All previously installed packages are reinstalled, with the same
installation reason as before. It assumes that the packages exist in the cache.
If they don't, the corresponding line in the file will be commented out and user
intervention will be required.
"""

import argparse
import os
import sys
from pipes import quote
from collections import deque
from pycman import config



parser = argparse.ArgumentParser(description='Generate scripts for toplogical re-installation of the given packages.')
parser.add_argument(
  'pkgs', metavar='<pkgname>', nargs='+',
  help='the packages to reinstall'
)
parser.add_argument(
  '-c', '--config', metavar='<path>', default='/etc/pacman.conf',
  help='Pacman configuration file'
)
parser.add_argument(
  '-o', '--out', metavar='<path>', default='.',
  help='The output directory. Default: %(default)s'
)



def get_manual_reinstall_cmd(operation, pkgs, reason=None, comment=False):
  if reason == 0:
    reason_arg = ' --asexplicit'
  elif reason is not None:
    reason_arg = ' --asdeps'
  else:
    reason_arg = ''

  was_str = isinstance(pkgs, str)
  if was_str:
    pkgs = [pkgs]

  if comment:
    placeholder = '# '
  else:
    placeholder = ''

  if pkgs:
    intercalator = ' \\\n{placeholder}  '
    cmd = '{{placeholder}}pacman {{operation}}{{reason}}{}{{pkgs}}\n'.format(intercalator).format(
      operation=operation,
      pkgs=intercalator.format(placeholder=placeholder).join(quote(p) for p in pkgs),
      placeholder=placeholder,
      reason=reason_arg,
    )
  else:
    cmd = ''
  if not was_str:
    del pkgs[:]
  return cmd



def main(args=None):
  pargs = parser.parse_args(args)
  roots = pargs.pkgs
  # Initialize the Pacman database.
  h = config.init_with_config(pargs.config)

  # Removal command
  rm_cmd = 'pacman -Rsc {}\n'.format(' '.join(quote(r) for r in roots))

  # Find all packages that depend on the given packages.
  queue = deque(roots)
  required_by = dict()
  while queue:
    pkg = queue.popleft()
    required_by[pkg] = h.get_localdb().get_pkg(pkg).compute_requiredby()
    for p in required_by[pkg]:
      if p not in required_by and p not in queue:
        queue.append(p)

  # Simple algorithm to determine the dependency level of each package.
  level = {}
  for root in roots:
    level[root] = 0
  queue = deque(roots)
  while queue:
    pkg = queue.popleft()
    for p in required_by[pkg]:
      try:
        if level[pkg] + 1 > level[p]:
          level[p] += 1
          queue.append(p)
      except KeyError:
        level[p] = level[pkg] + 1
        queue.append(p)

  # Sort the package list in the required build order.
  queue = list(h.get_localdb().get_pkg(pkg) \
    for pkg, lvl in sorted(level.items(), key=lambda x: (x[1], x[0])))


  explicit = set()
  implicit = set()
  in_repos = set()
  exts = ('.xz', '.gz')
  reinstall_from_cache = '''#!/bin/bash
set -e

# This script will reinstall package from the cache toplogically. Use it if
# the reinstall.sh script fails for some reason (e.g. due to unavailable
# repo packages).

# Commented commands mean that the package could not be found in the cache.
# Manual intervention will be required. Simply update the package path, and
# change the extension if necessary.

'''
  reinstall_from_cache += rm_cmd

  pending = list()
  last_reason = None
  last_found = None

  for pkg in queue:
    reason = pkg.reason
    path = None

    if reason == 0:
      explicit.add(pkg.name)
      for db in h.get_syncdbs():
        if db.get_pkg(pkg.name):
          in_repos.add(pkg.name)
          break
      else:
        sys.stderr.write('warning: not found in repos: {}\n'.format(pkg.name))
    else:
      implicit.add(pkg.name)

    # Search for the package in the cache dirs.
    basename = '{}-{}-{}.pkg.tar'.format(pkg.name, pkg.version, pkg.arch)
    for cache in h.cachedirs:
      for ext in exts:
        path = os.path.join(cache, basename + ext)
        if os.path.isfile(path):
          found = True
          break
      else:
        continue
      break
    else:
      # Manual intervention will be required.
      path = basename + exts[0]
      found = False
      sys.stderr.write('warning: not found in cache: {}\n'.format(pkg.name))

    if reason != last_reason or found != last_found:
      reinstall_from_cache += get_manual_reinstall_cmd('-U', pending, reason=last_reason, comment=(not last_found))
    else:
      pending.append(path)

    last_reason = reason
    last_found = found


  if pending:
    reinstall_from_cache += get_manual_reinstall_cmd('-U', pending, reason=last_reason, comment=(not last_found))


  outdir = pargs.out

  # Save the cache pkg script.
  with open(os.path.join(outdir, 'reinstall_from_cache.sh'), 'w') as f:
    f.write(reinstall_from_cache)

  # Save the list of affected packages for reference.
  # This can be used to restore the system if necessary.
  with open(os.path.join(outdir, 'pkglist.txt'), 'w') as f:
    if explicit:
      f.write("# Explicitly installed.\n")
      for pkg in sorted(explicit):
        f.write(pkg + "\n")
      if implicit:
        f.write("\n")

    if implicit:
      f.write("# Implicitly installed.\n")
      for pkg in sorted(implicit):
        f.write(pkg + "\n")

  # Save the topological reinstall script.
  with open(os.path.join(outdir, 'reinstall.sh'), 'w') as f:
    f.write('''#!/bin/bash
set -e

# Remove the target packages and all deps, then reinstall the explicitly
# installed packages. Commented commands are included for packages which could
# not be found in the current repos.

''')
    f.write(rm_cmd)
    f.write(get_manual_reinstall_cmd('-S', sorted(explicit & in_repos)))
    f.write(get_manual_reinstall_cmd('-S', sorted(explicit - in_repos), comment=True))




if __name__ == "__main__":
  try:
    main()
  except KeyboardInterrupt:
    pass

# vim: set ts=4 sw=4 tw=0 noet:
