#!/var/local/bin/python3.bash
# -*-mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-

from __future__ import unicode_literals, print_function

"""


"""

import os,sys,io
import argparse
import random
import base64
import curses

from keepassxc_doc import (dCLI_HELP, dKPY_HELP,)

try:
    from dialog import Dialog
except ImportError:
    Dialog = None
oDIALOG = None
oLAST_DIR = '/tmp/'

try:
    import keyutils
except ImportError:
    keyutils = None

oKP=None

sGridDir = '/usr/local/src/gridfire'
if os.path.isdir(sGridDir):
    sGridDir in sys.path or sys.path.append(sGridDir)

lCLI_WORDS_key_dbx = [ 'add',  'analyze',  'clip',  'close',
                          'db-create',  'db-info',
                          'edit',  'estimate',  'export',  'format',
                          'import',  'locate',  'ls',  'merge',  'mkdir',
                          'mv',  'open',  'rm',  'rmdir',  'show']

lCLI_WORDS_nokey_nodbx = ['help', 'generate',   'diceware',]
cli_item_strs = lCLI_WORDS_nokey_nodbx + lCLI_WORDS_key_dbx

kpy_choices = ['open', 'eval', 'save', 'close', 'dump_xml', 'help']
kpy_new = ['add_binary_base64', 'get_binary_base64', 'get_binary_file']
kpy_methods = ['add_binary', 'add_entry', 'add_group', 'attachments', 'binaries', 'delete_binary', 'delete_entry', 'delete_group', 'deref', 'dump_xml', 'encryption_algorithm', 'entries', 'filename', 'find_attachments', 'find_entries', 'find_entries_by_notes', 'find_entries_by_password', 'find_entries_by_path', 'find_entries_by_string', 'find_entries_by_title', 'find_entries_by_url', 'find_entries_by_username', 'find_entries_by_uuid', 'find_groups', 'find_groups_by_name', 'find_groups_by_notes', 'find_groups_by_path', 'find_groups_by_uuid', 'groups', 'kdbx', 'kdf_algorithm', 'keyfile', 'move_entry', 'move_group', 'password', 'read', 'root_group', 'save', 'transformed_key', 'tree', 'version', 'xml'] + kpy_new

sGridDir = '/usr/local/src/cmd2'
if os.path.isdir(sGridDir):
    sGridDir in sys.path or sys.path.append(sGridDir)
import cmd2
from cmd2_ansi import *

oSCR=None

class CmdLineApp(cmd2.Cmd):
    """keepassxc cmd2 application using keepassxc-cli and pykeepass."""

    # Setting this true makes it run a shell command if a cmd2/cmd command doesn't exist
    # default_to_shell = True
    def __init__(self):
        self.sPassword = ''
        self.sKeyfile = ''
        self.sKdbx = ''
        self.sGridfire = ''
        self.oPyKdbx = ''
        shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
        shortcuts.update({'&': 'kpy'})
        super().__init__(allow_cli_args=False, multiline_commands=['orate'], shortcuts=shortcuts)

        self.self_in_py = True

        # Make sPassword settable at runtime
        self.add_settable(cmd2.Settable('sPassword', str, 'password for keepassxc', self))
        # Make sKeyfile settable at runtime
        self.add_settable(cmd2.Settable('sKeyfile', str, 'keyfile for keepassxc', self))
        # Make sKdbx settable at runtime
        self.add_settable(cmd2.Settable('sKdbx', str, 'kdbx for keepassxc', self))

        # Make sGridfire settable at runtime
        self.add_settable(cmd2.Settable('sGridfire', str, 'gridfire for keepassxc', self))

        # Should ANSI color output be allowed
        if not hasattr(sys, 'frozen') and ansi:
           self.allow_style = ansi.AllowStyle.TERMINAL

    clean_parser = cmd2.Cmd2ArgumentParser()
    clean_parser.add_argument('-r', '--recycle', type=int, help='Just clean the recycle bin')

    @cmd2.with_argparser(clean_parser)
    def do_clean_kdbx(self, args):
        """Cleans all of the entries and groups from a kdbx"""

        if self.oPyKdbx == '':
            self.poutput(red('cle: ! ') + 'Open database first' )
            return

        kp = self.oPyKdbxf
        for group in kp.groups:
            if group != kp.root_group:
                _=sys.stderr.write( 'deleting group ' +str(group) +'\n')
                kp.delete_group(group)
            else:
                entries = kp.entries
                for entry in entries:
                    _=sys.stderr.write( 'deleted entries ' +str(entry) +'\n')
                    entry.delete()

    getpass_parser = cmd2.Cmd2ArgumentParser()
    getpass_parser.add_argument('words', nargs='*', help='arguments')
    @cmd2.with_argparser(getpass_parser)
    def do_get_pass(self, args):
        """Get the password, and set the variable sPassword."""
        if args.words:
            if args.words[0] == '-':
                self.sPassword = sys.stdin.read()
            else:
                self.sPassword = args.words[0]
            self.poutput(green('Pas: . ') +self.sPassword)
            return
        from gridfire import getpass
        if self.sKdbx:
            sPrompt = 'Password for ' +self.sKdbx
        else:
            sPrompt = 'Password for keepassxc_cmd2'
        oTty = os.ttyname(sys.__stdin__.fileno())
        oRet = getpass(prompt=sPrompt, stream=None, tty=oTty, main_args=None)
        if oRet:
            self.sPassword = oRet
            self.poutput(green('Pas: . ') +self.sPassword)
    do_get_sPassword = do_get_pass

    getkeyfile_parser = cmd2.Cmd2ArgumentParser()
    getkeyfile_parser.add_argument('words', nargs='*', help='arguments')
    @cmd2.with_argparser(getkeyfile_parser)
    def do_get_keyfile(self, args):
        """Get the name of the key file, and set the variable sKeyfile."""
        if len(args.words) and os.path.isfile(args.words[0]):
            self.sKeyfile = args.words[0]
            self.poutput(green('Key: . ') +str(self.sKeyfile))
            return

# you will first need to save the tty modes with a call to def_prog_mode()
# and then call endwin() to end the curses mode. This will leave you in the
# original tty mode. To get back to curses once you are done, call
# reset_prog_mode() . This function returns the tty to the state stored by
# def_prog_mode(). Then do refresh(), and you are back to the curses mode.
        from gridfire import lDialogFselect
        oRet = lDialogFselect()
        try:
            curses.def_prog_mode()
            curses.endwin()
        except Exception as e:
            # must call initscr() first
            # global oSCR; if oSCR is none:  oSCR=curses.initscr()
            pass

        if len(oRet) == 2 and oRet[0] == 'ok':
            self.sKeyfile = oRet[1]
            self.poutput(green('key: . ') +str(self.sKeyfile))
    do_get_sKeyfile = do_get_keyfile

    getkdbx_parser = cmd2.Cmd2ArgumentParser()
    getkdbx_parser.add_argument('words', nargs='*', help='arguments')
    @cmd2.with_argparser(getkdbx_parser)
    def do_get_kdbx(self, args):
        """Get the name of the kdbx database file, and set the variable sKbx."""
        if len(args.words):
            self.sKdbx = args.words[0]
            if os.path.isfile(args.words[0]):
                self.poutput(green('Kdb: . ') +str(self.sKdbx))
            else:
                self.poutput(red('Kdb: ! File not found ') +str(self.sKdbx))
            return

        from gridfire import curses
# you will first need to save the tty modes with a call to def_prog_mode()
# and then call endwin() to end the curses mode. This will leave you in the
# original tty mode. To get back to curses once you are done, call
# reset_prog_mode() . This function returns the tty to the state stored by
# def_prog_mode(). Then do refresh(), and you are back to the curses mode.
        from gridfire import lDialogFselect
        oRet = lDialogFselect()
        try:
            # Is this needed? Between FSelect and gridfire is the idea
            # but it errors with initscr() not called when called here
            curses.def_prog_mode(); curses.endwin()
        except Exception: pass

        if len(oRet) == 2 and oRet[0] == 'ok':
            self.sKdbx = oRet[1]
            self.poutput(green('dbx: . ') +str(self.sKdbx))
    do_get_sKdbx = do_get_kdbx

    def cli_help(sel, sElt):
        if sElt and sElt in dCLI_HELP:
            self.poutput(green('cli: help ') +sElt +dCLI_HELP[sElt])
        else:
            self.poutput(green('kpy: help ') +'one of: ' \
                         +' '.join(dCLI_HELP.keys()))

    qdbus_parser = cmd2.Cmd2ArgumentParser()
    qdbus_parser.add_argument('words', nargs='*', help='arguments')
    @cmd2.with_argparser(qdbus_parser)
    def do_qdbus(self, args):
        """."""

        MainWindow='org.keepassxc.KeePassXC.MainWindow'
        def qdbus(s): os.system('qdbus ' +s)

        if not args.words or args.words[0]  == "help":
            self.poutput(green('bus: ? ') +'one of: ' \
                         'open close exit')
            return

        sCommand = args.words[0]
        if  sCommand == "open":
            #    Open keepassxc database: without password and key file
            qdbus # MainWindow +' /keepassxc ' +MainWindow.openDatabase /path/to/database.kdbx

        elif  sCommand == "open":
            #    Open keepassxc database: with password but without key file
            qdbus # MainWindow +' /keepassxc ' +MainWindow.openDatabase /path/to/database.kdbx passwd

        elif  sCommand == "open":
           #    Open keepassxc database: with password and key file
            qdbus # MainWindow +' /keepassxc ' +MainWindow.openDatabase /path/to/database.kdbx passwd /path/to/key

        elif  sCommand == "lock":
            #    Lock all keepassxc databases
            qdbus # MainWindow +' /keepassxc ' +MainWindow.lockAllDatabases
        elif  sCommand == "close":
            #    Close all keepassxc databases
            qdbus # MainWindow +' /keepassxc ' +MainWindow.closeAllDatabases

        elif  sCommand == "exit":
            #    Exit keepassxc
            qdbus(MainWindow +' /keepassxc ' +MainWindow +'.exit')

        return

    cli_parser = cmd2.Cmd2ArgumentParser()
    cli_parser.add_argument('cli', help='Enter name cli command', choices=cli_item_strs)
    cli_parser.add_argument('words', nargs='*', help='cli command arguments')
    @cmd2.with_argparser(cli_parser)
    def do_cli(self, args):
        """Call keepassxc-cli with the kdbx database file and a cli command."""
        sStdIn = ''
        if args.cli in ['help', '--help', ]:
            if args.words and args.words[0] in dCLI_HELP:
                sElt = args.words[0]
                self.poutput(green('cli: ? ') +sElt +dCLI_HELP[sElt])
            else:
                self.poutput(green('cli: ? ') +'one of: ' \
                             +' '.join(dCLI_HELP.keys()))
            return
        if args.words and args.words[0] in ['help', '--help', ]:
            sElt = args.cli
            return
        if args.cli in lCLI_WORDS_nokey_nodbx:
            largs = ['keepassxc-cli.bash', args.cli] + args.words
        elif args.cli in lCLI_WORDS_key_dbx:
            if self.sPassword == '':
                self.poutput(red('cli: ! ') + 'We require a password using get_pass' )
                return
            sStdIn = self.sPassword
            if not self.sKdbx or not os.path.isfile(self.sKdbx):
                self.poutput(red('cli: ! ') + 'We require a kbx file using get_kdbx' )
                return
            if not self.sKeyfile or not os.path.isfile(self.sKeyfile):
                self.poutput(yellow('cli: = ') + 'Not using a keyfile' )
                largs = ['keepassxc-cli.bash', args.cli]
            else:
                largs = ['keepassxc-cli.bash', args.cli, '-k', self.sKeyfile]
            largs += ['-q', self.sKdbx] +args.words

        if len(sStdIn):
            self.poutput(blue('cli: . ') +' Using a password of len' +str(len(sStdIn)))
        from gridfire import lSubprocess
        self.poutput(blue('cli: < ') +' Calling ' +' '.join(largs))
        shell = False
        executable = '' # largs[0]        sargs = ' '.join(largs[1:])
        (exit_code, sStdout,) = lSubprocess(largs, shell, executable, sStdIn)
        if exit_code == 0:
            if sStdout is not None:
                if sStdout.find('\n') > 0:
                    for sLine in sStdout.split('\n'):
                        self.poutput(green('cli: > ') +sLine)
                else:
                    self.poutput(green('cli: > ') +repr(sStdout))
        else:
            if sStdout is not None:
                self.poutput(red('cli: > ') +repr(sStdout))

    kpy_parser = cmd2.Cmd2ArgumentParser()
    clean_parser.add_argument('-K', '--session_add_key', type=str,
                              help='session_add_key some-description')
    kpy_parser.add_argument('kpy', help='Enter name kpy command',
                            choices=kpy_choices+kpy_methods+['_xpath'])
    kpy_parser.add_argument('words', nargs='*', help='kpy command arguments')
    @cmd2.with_argparser(kpy_parser)
    def do_kpy(self, args):
        """Call the Python oKP with the database  and a kpy method or attribute."""
        global oKP
        if args.kpy in ['help', '--help', ]:
            if args.words and args.words[0] in dKPY_HELP:
                sElt = args.words[0]
                self.poutput(green('kpy: help ') +sElt  +' - ' +dKPY_HELP[sElt])
            else:
                self.poutput(green('kpy: help ') +'one of: ' \
                             +' '.join(dKPY_HELP.keys()))
            return
        if args.words and args.words[0] in ['help', '--help', ]:
            sElt = args.kpy
            if sElt and sElt in dKPY_HELP:
                self.poutput(green('kpy: help ') +sElt +' - ' +dKPY_HELP[sElt])
            else:
                self.poutput(green('kpy: help ') +'one of: ' \
                             +' '.join(dKPY_HELP.keys()))
            return

        if args.kpy == 'open' and self.oPyKdbx != '':
            self.poutput(red('kpy: ! ') + 'Close the open database first' )
            return
        if args.kpy == 'open' and self.oPyKdbx == '':
            if not self.sKdbx or not os.path.isfile(self.sKdbx):
                self.poutput(red('kpy: ! ') + 'We require a kbx file using get_kdbx' )
                return

            if self.sPassword == '':
                self.poutput(red('kpy: ! ') + 'We require a password using get_pass' )
                return

            if not self.sKeyfile or not os.path.isfile(self.sKeyfile):
                self.poutput(yellow('kpy: = ') + 'Not using a keyfile' )

            from pykeepass import PyKeePass
            try:
                oKP = PyKeePass(self.sKdbx, password=self.sPassword, keyfile=self.sKeyfile or None)

            except Exception as e:
                self.poutput(red('kpy: ! ') + 'Failed opening a database ' +str(e))
                return

            oKP.filename = self.sKdbx
            oKP.password = self.sPassword
            oKP.keyfile =  self.sKeyfile or None
            #             oKP.save()
            self.oPyKdbx = oKP
            self.poutput(green('kpy: . ') + 'Opened a database ' +repr(oKP))
            if args.session_add_key:
                yVal = self.password.encode()
                yDesc = args.session_add_key.encode()
                session_add_key(yDesc, yVal)
g`<                self.poutput(green('kpy: . ') + 'Added session key ' +args.session_add_key)
            return
        if args.kpy in ['close', 'save']+kpy_methods and not self.oPyKdbx:
            self.poutput(red('kpy: ! ') + 'Open a database before using ' +args.kpy )
            return
        if args.kpy == 'close' and self.oPyKdbx:
            self.oPyKdbx = oKP = None
            self.poutput(green('kpy: . ') + 'Closed the open database' )
            return
        if args.kpy in ['read', 'dump_xml'] and self.oPyKdbx:
            if len(args.words) < 1:
                self.poutput(red('kpy: ! ') + 'Filename required to ' +args.kpy +' a database' )
                return
            sLine = args.words[0]
            if args.kpy in ['read']:
                self.poutput(blue('kpy: . ') +args.kpy +' kdbx from '+sLine)
            elif args.kpy in ['dump_xml']:
                self.poutput(blue('kpy: . ') +args.kpy +' kdbx to '+sLine)
            oMeth = getattr(self.oPyKdbx, args.kpy)
            o = oMeth(*args.words)
            self.poutput(green('kpy: < ') + args.kpy +' ' +repr(o))
            return

        if args.kpy in kpy_methods+['_xpath'] and self.oPyKdbx: # 'save'

            if args.kpy in [  'add_entry', 'add_group']:
                self.poutput(yellow('kpy: = ') + 'Unsupported: ' +args.kpy )
                return

            if args.kpy == 'get_binary_base64':
                oMeth = getattr(self.oPyKdbx, 'binaries')[int(args.words[0])]
                oMeth = base64.b64encode(oMeth)
            elif args.kpy == 'add_binary_base64':
                args.kpy = 'add_binary'
                oMeth = getattr(self.oPyKdbx, args.kpy)
                args.words = [base64.b64decode(args.words[0])]
                # compressed=True, protected=True
            elif args.kpy == 'add_binary_file':
                args.kpy = 'add_binary'
                oMeth = getattr(self.oPyKdbx, args.kpy)
                with open(args.words[0], 'rb') as iFd:
                    oMeth = iFd.read()
                # compressed=True, protected=True
            else:
                oMeth = getattr(self.oPyKdbx, args.kpy)
            if args.kpy == 'delete_binary':
                args.words = [int(args.words[0])]
            if not callable(oMeth):
                o = oMeth
            elif len(args.words):
                o = oMeth(*args.words)
            else:
                o = oMeth()

            if type(o) is type(None) or o is None:
                s = ''
            elif type(o) in [list, tuple] and len(o):
                # xpath
                if 'text' in dir(o[0]):
                    #  lxml.etree._Element
                    s = o[0].text
                else:
                    s = repr(list(map(str,o)))
            elif type(o) == str:
                s = o
            else:
                s = repr(o)

            self.poutput(green('kpy: < ') +args.kpy +': ' +s )
            return
        if args.kpy in ['exec', 'eval'] and not self.oPyKdbx:
            self.poutput(yellow('kpy: = ') + 'Usually open a database first' )

        if args.kpy == 'eval':
            from io import StringIO

            sLine = ' '.join(args.words)
            self.poutput(blue('kpy: > ') +str(sLine))
            old = sys.stdout
            exit_code = 0
            try:
                sys.stdout  = StringIO()
                dLocals = {}
                dLocals['oKP'] = oKP
                if True:
                    # sStdout = eval(compile(sLine, 'single', 'exec'), globals(), dLocals)
                    sStdout = eval(sLine, globals(), dLocals)
                else:
                    exec(compile(sLine, 'single', 'exec'))
                    sys.stdout.seek(0)
                    sStdout = sys.stdout.read()
            finally:
                sys.stdout = old

            if not sStdout:
                pass
            elif exit_code == 0:
                self.poutput(green('kpy: < ') +str(sStdout))
            else:
                self.poutput(red('kpy: ! ') +str(sStdout))

def session_add_key(yDesc,yVal):

    from keyutils import join_session_keyring, add_key, request_key, set_perm

    iSk=join_session_keyring()
    assert type(iSk) == int

    iKey = add_key(yDesc, yVal, iSk)
    assert iKey >= 0

    key_id=request_key(yDesc, iSk)
    assert key_id > 0

    set_perm(key_id, 0x3f1f0000)
    info('session_add_key(' +yDesc +')')


def main(argv=None):
    """Run when invoked from the operating system shell"""

    parser = cmd2.Cmd2ArgumentParser(description='Commands as arguments')
    command_help = 'optional command to run, if no command given, enter an interactive shell'
    parser.add_argument('command', nargs='?', help=command_help)
    arg_help = 'optional arguments for command'
    parser.add_argument('command_args', nargs=argparse.REMAINDER, help=arg_help)

    args = parser.parse_args(argv)

    c = CmdLineApp()

    sys_exit_code = 0
    if args.command:
        # we have a command, run it and then exit
        c.onecmd_plus_hooks('{} {}'.format(args.command, ' '.join(args.command_args)))
    else:
        # we have no command, drop into interactive mode
        sys_exit_code = c.cmdloop()

    return sys_exit_code

if __name__ == '__main__':
    try:
        sys.exit(main())
    except io.UnsupportedOperation:
        pass

