#!/usr/bin/env python3

import argparse
import colorsysplus
import os
import sys
import time
import urllib.error
import xdg.BaseDirectory



################################### Globals ####################################

# Copy XCGF.DISPLAY_TIME_FORMAT
TIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z'

NAME = 'bb-query_trust'
USERS = 'users.txt'
COMBINATIONS = 'combinations.txt'

EXIT_UNTRUSTED = 'untrusted'



############################### Argument Parsing ###############################

parser = argparse.ArgumentParser(
  prog=NAME,
  description='Determine if the given combination of package base, maintainer and release time should be trusted. Prompt the user if not.',
  epilog='To reset %(prog)s, remove {} and {} from the configuration file directory.'.format(USERS, COMBINATIONS)
)

parser.add_argument(
  'pkgbase', metavar='<package base>',
  help='The package base.'
)
parser.add_argument(
  'mtime', type=int, metavar='<modification time>',
  help='The modification time, in UNIX format.'
)
parser.add_argument(
  'maintainers', nargs='*', metavar='<maintainer>',
  help='The maintainer(s).'
)
parser.add_argument(
  '-c', '--config', metavar='<path>', default=xdg.BaseDirectory.save_config_path(NAME),
  help='The configuration file directory. It will contain 2 files: {} and {}. The former lists the users to trust one per line. The latter lists combinations to trust, with each line containing the package base, the modification time (UNIX format), and the package maintainers. You can use "aurtus" from "python3-aur" to populate the users file with a list of TUs. Default configuration directory: %(default)s'
)
parser.add_argument(
  '-l', '--location', metavar='<path>',
  help='The path to the directory containing the PKGBUILD and related file. This is only to prompt the user with the location of the files while waiting.'
)
parser.add_argument(
  '--nocolor', action='store_true',
  help='Disable color output.'
)



############################# Configuration Files ##############################

def load_lines(path):
  try:
    with open(path, 'r') as f:
      for l in f:
        l = l.strip()
        if not l or l[0] == '#':
          continue
        else:
          yield l
  except FileNotFoundError:
    pass

def save_lines(path, ls):
  with open(path, 'w') as f:
    for l in sorted(ls):
      f.write(l + '\n')



def load_tuples(path):
  for l in load_lines(path):
    yield l.split()

def save_tuples(path, ts):
  save_lines(path, (' '.join(t) for t in ts))



def load_combinations(path):
  combinations = dict()
  for t in load_tuples(path):
    pkgbase = t[0]
    if t[1] == '*':
      mtime = None
    else:
      mtime = int(t[1])
    ms = set(t[2:])
    combinations[pkgbase] = (mtime, ms)
  return combinations

def combinations_to_tuples(combinations):
  for c, x in combinations.items():
    mtime, ms = x
    if mtime is None:
      mtime = '*'
    else:
      mtime = str(mtime)
    yield (c, mtime) + tuple(sorted(ms))

def save_combinations(path, combinations):
  save_tuples(path, combinations_to_tuples(combinations))



#################################### Prompt ####################################

def prompt_user(pkgbase, mtime, maintainers, tus, location=None, color=True):
  if color:
    pkgbase_ce = colorsysplus.ansi_sgr(fg=14)
    mtime_ce = colorsysplus.ansi_sgr(fg=13)
    maintainer_ce = colorsysplus.ansi_sgr(fg=10)
    path_ce = colorsysplus.ansi_sgr(fg=12)
    highlight_ce = colorsysplus.ansi_sgr(fg=11)
    reset_ce = colorsysplus.ansi_sgr(reset=True)
  else:
    pkgbase_ce = ''
    mtime_ce = ''
    maintainer_ce = ''
    path_ce = ''
    highlight_ce = ''
    reset_ce = ''


  recognized_options = {
    'P': 'Trust this combination once and proceed.',
    'M': 'Add maintainer(s) to list of trusted users.',
    'C': 'Add pkgbase-maintainer(s)-timestamp combination to list of trusted combinations.',
    'T': 'Add pkgbase-maintainer(s) combination to list of trusted combinations.'
  }

  if maintainers:
    valid_options = 'PMCT'
  else:
    valid_options = 'PC'

  option_txt = '\n'.join(
    '  {}{}{}: {}'.format(
      highlight_ce,
      o,
      reset_ce,
      recognized_options[o]
    ) for o in valid_options
  )

  if maintainers:
    l = max(len(m) for m in maintainers)
    ms_lines = list()
    for m in sorted(maintainers):
      if m in tus:
        m = m.ljust(l) + ' {}[trusted]{}'.format(highlight_ce, reset_ce)
      ms_lines.append(m)
    ms = '\n                  \t'.join(ms_lines)
  else:
    ms = '{}[orphan]{}'.format(highlight_ce, reset_ce)



  if location:
    loc = '''You may inspect the files at the following location before proceeding:
  {path_ce}{path}{reset_ce}

'''.format(
      path=os.path.abspath(location),
      path_ce=path_ce,
      reset_ce=reset_ce
    )
  else:
    loc = ''

  try:
    repo, pkgbase = pkgbase.split('/', 1)
  except ValueError:
    pb = '{}{}{}'.format(pkgbase_ce, pkgbase, reset_ce)
  else:
    pb = '{path_ce}{repo}{reset_ce}/{pkgbase_ce}{pkgbase}{reset_ce}'.format(
      path_ce=path_ce,
      pkgbase_ce=pkgbase_ce,
      reset_ce=reset_ce,
      repo=repo,
      pkgbase=pkgbase
    )


  prompt = '''Untrusted combination:
  Package Base:     \t{pkgbase}
  Modification Time:\t{mtime_ce}{dtime}{reset_ce} {highlight_ce}[{mtime:d}]{reset_ce}
  Maintainer(s):    \t{maintainer_ce}{ms}{reset_ce}

{loc}Options:
{opts}

  Press any other key if you do not trust this combination.

[{vopts}] '''.format(
    pkgbase=pb,
    mtime=mtime,
    dtime=time.strftime(TIME_FORMAT, time.gmtime(mtime)),
    ms=ms,
    loc=loc,
    opts=option_txt,
    vopts=valid_options,
    mtime_ce=mtime_ce,
    maintainer_ce=maintainer_ce,
    highlight_ce=highlight_ce,
    reset_ce=reset_ce
  )


  ans = input(prompt).upper()
  if not ans or ans[0] not in valid_options:
    sys.exit(EXIT_UNTRUSTED)
  return ans[0]



##################################### Main #####################################

def main(args=None):
  pargs = parser.parse_args(args)

  users_path = os.path.join(pargs.config, USERS)
  combinations_path = os.path.join(pargs.config, COMBINATIONS)

  tus = set(load_lines(users_path))
  combinations = load_combinations(combinations_path)

  maintainers = set(pargs.maintainers)

  if maintainers and tus & maintainers == maintainers:
    # All maintainers are trusted.
    return

  try:
    mtime, ms = combinations[pargs.pkgbase]
  except KeyError:
    pass
  else:
    # The package-mtime combination is permitted even if there are no
    # maintainers. The user can trust the last release even if it is an orphan.
    if (mtime is None or pargs.mtime == mtime) and (maintainers & ms) == maintainers:
      # This combination is trusted.
      return


  c = prompt_user(
    pargs.pkgbase,
    pargs.mtime,
    maintainers,
    tus,
    location=pargs.location,
    color=(sys.stdout.isatty() and not pargs.nocolor),
  )
  # M and T will not be returned if there are no maintainers.
  if c == 'M':
    tus |= maintainers
    save_lines(users_path, tus)
  elif c == 'C':
    combinations[pargs.pkgbase] = (pargs.mtime, maintainers)
    save_combinations(combinations_path, combinations)
  elif c == 'T':
    combinations[pargs.pkgbase] = (None, maintainers)
    save_combinations(combinations_path, combinations)



if __name__ == '__main__':
  try:
    main()
  except (KeyboardInterrupt, BrokenPipeError):
    sys.exit(EXIT_UNTRUSTED)
