#!/usr/bin/env python3
# coding: utf-8
# PYTHON_ARGCOMPLETE_OK
import sys
import argparse
from time import time as now
import signal
import subprocess
import getpass

try:
    import pyperclip

    def to_clipboard(s):
        pyperclip.copy(s)
except ImportError:
    def to_clipboard(s):
        raise Exception("unable to copy to clipboard")
try:
    import argcomplete
except ImportError:
    argcomplete = None

escape_codes = (0, 1, 2, 94, 47, 91)
try:
    import colorama
    colorama.init(autoreset=True)
except ImportError:
    colorama = None

RESET, BRIGHT, DIM, FORE, BACK, FORE2 = [
    '' if colorama is None else '\x1b[{}m'.format(n) for n in escape_codes]

import pympw


default_counter = 1
default_type = 'long'
help_cmds = ['?', 'help']
quit_cmds = ['quit']


def input_format(s):
    return BRIGHT + s + RESET + BRIGHT + ' >' + RESET + ' '


def splitby_completer(**kwargs):
    return ('\t', ' ', '/', ':')


def main():
    parser = argparse.ArgumentParser(
        description='CLI to Master Password algorithm v3. Passwords are generated locally, your master password is not sent to any server. http://masterpassword.app')
    parser.add_argument('--name', '-n', type=str, default=None,
                        help='your full name')
    parser.add_argument('--site', '-s', type=str, default=None,
                        help='site name (e.g. linux.org). omit this argument to start an interactive session.')
    parser.add_argument('--counter', '-c', type=int, default=default_counter,
                        help='positive integer less than 2**31=4294967296')

    types = pympw.template_class_names
    parser.add_argument('--type', '-t', metavar='TYPE', type=str, 
                        default=default_type, choices=types,
                        help='password type, one of ' + ', '.join(types))

    parser.add_argument(
        "--quiet", "-q", action="store_true", help="less output")
    parser.add_argument('--copy', '-y', action='store_true',
                        help='copy password to clipboard instead of printing it')
    parser.add_argument('--hide-pw', '-d', action='store_true',
                        help='never print passwords')
    parser.add_argument('--splitby', '-b', metavar='CHAR', type=str, default=None,
                        help="more efficient interactive session. suggested values: tab, space, or '/'"
                        ).completer = splitby_completer
    parser.add_argument("--keepalive", "-k", action="store_true",
                        help="keep program from timing out by pressing ENTER")
    parser.add_argument('--exit-after', '-e', metavar='T', type=int,
                        default=None, help='script will timeout and close after this many seconds')
    parser.add_argument("--exit-command", metavar='COMMAND', type=str,
                        default=None, help="run this command if the script times out")
    parser.add_argument('--no-color', "--strip-ansi", action='store_true',
                        help="suppress colored output in interactive session")

    # read site, counter, and type until quit
    if not argcomplete is None:
        argcomplete.autocomplete(parser)
    args = parser.parse_args(sys.argv[1:])

    if args.hide_pw:
        args.copy = True

    def verbose_print(*_args, **kwargs):
        if not args.quiet:
            print(*_args, **kwargs)

    # read Name if not passed as argument
    # read password without echo
    courtesy = '' if args.quiet else 'please type your '
    try:
        verbose_print(FORE + BACK + "Welcome to Master Password")
        if args.name is None:
            args.name = input(input_format(
                FORE + BACK + courtesy + 'full name'))

        master_pw = getpass.getpass(input_format(
            FORE2 + BACK + courtesy + 'password '))

        # precompute master key
        key = pympw.master_key(args.name, master_pw)
        # del master_pw
        # del args.name
    except (EOFError, KeyboardInterrupt, ValueError) as e:
        print(e)
        sys.exit(1)

    # print site password if site name was passed as argument
    if not args.site is None:
        verbose_print(DIM + 'site={}, type={}, counter={}'.format(args.site,
                                                                  args.type, args.counter))
        print(pympw.site_password(key, args.site, args.type, args.counter))
        return

    # loop if no site name was given
    if not args.exit_after is None:
        # start a chain of ALARM signals that periodically check if the program
        # must close now
        handler_params = {
            "active": now(),
            "scheduled": now()
        }

        def handler(signum, frame):
            p = handler_params
            active, scheduled = p["active"], p["scheduled"]

            def active_since(t): return now() - (1 + active) < t
            next_delay = max(1, round(active - scheduled))
            # print('ALARM!', now() - active, active_since(args.exit_after), active, next_delay, active - scheduled)
            if not active_since(args.exit_after):
                raise InterruptedError
            else:
                p["scheduled"] = now()
                signal.alarm(next_delay)
        signal.signal(signal.SIGALRM, handler)
        signal.alarm(args.exit_after)

    courtesy = '' if args.quiet else 'please type '

    def get_input(prompt):
        r = input(input_format(prompt))
        if args.keepalive:
            handler_params["active"] = now()
        return r

    def get_param(name, default):
        x = get_input(courtesy + name + ('' if default ==
                                         '' else ' or ENTER for default=' + str(default)))
        if x.lower() in help_cmds:
            parser.print_help()
            return default
        if x.lower() in quit_cmds:
            raise EOFError  # TODO hacky
        return x if x != '' else default
    try:
        # run a loop that asks for site name, template class (aka password
        # type, and counter
        sb = args.splitby
        password = None
        while True:
            if not sb is None and len(sb) > 0 and not sb in ['\n']:
                ins = get_input(
                    courtesy + 'site name[{}type[{}counter]]'.format(sb, sb))
                # is this a request for help?
                if ins in help_cmds:
                    parser.print_help()
                    ins = ''
                if ins in quit_cmds:
                    break
                # split input by the given split-by character
                ins = ins.split(sb)
                site = ins[0] if len(ins) > 0 else ''
                _type = ins[1] if len(ins) > 1 else args.type
                counter = ins[2] if len(ins) > 2 else args.counter
            else:
                # get site, counter, and password type, or defaults
                site = get_param('site name', '')
                counter = get_param('site counter', args.counter)
                _type = get_param('site type', args.type)

            # nag until a non-empty site name is given
            if site == '':
                verbose_print(DIM + 'please enter a valid site name')
                continue
            try:
                counter = int(counter)
                if counter < 1:
                    raise ValueError
            except ValueError:
                verbose_print(DIM + "please enter a positive counter")
                continue
            if _type not in pympw.template_classes:
                verbose_print(
                    DIM + "please enter a valid template class/password type")
                continue

            # print(site, key, _type, counter)
            # print([type(v) for v in [site, key, _type, counter]])
            password = pympw.site_password(key, site, _type, counter)

            # either print password or copy it to clipboard
            if args.copy:
                try:
                    to_clipboard(password)
                    verbose_print('password copied to clipboard')
                except Exception as e:
                    print(e)
            if not args.hide_pw:
                print(password)
    except (EOFError, KeyboardInterrupt):
        pass
    except InterruptedError:
        # run command after timeout
        verbose_print(args.exit_after, 'second timeout reached')
        if not args.exit_command is None:
            subprocess.call(args.exit_command, shell=True)
    finally:
        print(DIM + 'bye')
        # del key
        # del password


if __name__ == '__main__':
    main()
