#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# PYTHON_ARGCOMPLETE_OK
from Crypto.Cipher import AES
import hashlib, binascii

import json
import getpass
import time
import subprocess
import os
import argparse, argcomplete
import sys
import keyring
from pysqlcipher3 import dbapi2 as sqlite

# To get all types of information decrypted run this:
# print(pad(field['label']) +  ": " + field['type'])

# Set up wallet variable. Change wallet variable to other location if needed
wallet = os.getenv('HOME') + '/Documents/Enpass/walletx.db'
password_store_decline = os.getenv('HOME') + '/Documents/Enpass/.store_decline'

def pad(msg):
    return " "*2 + msg.ljust(12)

def getScriptPath():
    return os.path.dirname(os.path.realpath(sys.argv[0]))

def abort(message):
    warn("Abort: " + message)
    sys.exit(1)

def warn(message):
    print(message, file=sys.stderr)

class Chooser:
    import os, sys

    def __init__(self, choices):
        self.choices = choices

    def appleScriptChooser(self):
        import tempfile
        """
        give user a choice of items. return selected item
        """
        self.SLABSCRIPT = """
                tell app "System Events"
                        Activate

                        set AccountList to {}
                        set Answer to (choose from list AccountList with title "Select Account")

                        if Answer is false then
                                error number -128 (* user cancelled *)
                        else
                                set Answer to Answer's item 1 (* extract choice from list *)
                        end if
                end tell
                tell app "iTerm2"
                        Activate
                        return Answer
                end tell
        """
        fd = tempfile.NamedTemporaryFile(delete=False)
        # Oh, AD, you special, special child...
        s = '{ "' + '", "'.join(sorted(self.choices)).replace('\\', '\\\\') + '" }'
        fd.write(self.SLABSCRIPT.format(s).encode('utf-8'))
        name = fd.name
        fd.close()

        try:
            out = subprocess.check_output(['/usr/bin/osascript', name], universal_newlines=True)
            out = out.rstrip()
        except:
            sys.exit()

        os.unlink(name)
        return out

    def chooseGUIChooser(self):
        """
        use choose gui for choices
        https://github.com/sdegutis/choose
        """
        try:
            out = subprocess.run(['/usr/local/bin/choose'], stdout=subprocess.PIPE, input='\n'.join(sorted(self.choices)), universal_newlines=True)
        except:
            sys.exit()
        return out.stdout.strip()

    def zenityGUIChooser(self):
        """
        use zenity gui for choices
        """
        try:
            out = subprocess.run(['/usr/bin/zenity', '--list', '--text', 'Please select the account', '--column', 'Accounts'], stdout=subprocess.PIPE, input='\n'.join(sorted(self.choices)), universal_newlines=True)
        except:
            sys.exit()
        return out.stdout.strip()

    def dumbCLIChooser(self):
        """
        use the dumb chooser... Rather this than figting with dialog!
        """
        def __pad(msg, length=12):
            return " "*2 + msg.ljust(length)

        cards = []
        print()

        for card in sorted(self.choices):
            cards.append(card)
            print(__pad(str(len(cards)) + '. ', 4) + card)

        try:
            print()
            selection = input('Select account (1-' + str(len(cards)) + '): ')
            selection = int(selection)-1
        except ValueError:
            print('Invalid selection')
            sys.exit(1)

        return cards[selection]
            
    def choice(self):
        """
        give user a choice of items, return selected item
        if choose-gui is installed, use that, otherwise fall back to applescript
        """
        if os.path.exists('/usr/local/bin/choose'):
            return self.chooseGUIChooser()
        elif sys.platform == 'darwin':
            return self.appleScriptChooser()
        elif os.path.exists('/usr/bin/zenity'):
            return self.zenityGUIChooser()
        else:
            return self.dumbCLIChooser()


class Enpassant:
    def __init__(self, filename, password):
        self.initDb(filename, password)
        self.crypto = self.getCryptoParams()

    def __getScriptPath(self):
        import os
        return os.path.dirname(os.path.realpath(sys.argv[0]))

    # Sets up SQLite DB
    def initDb(self, filename, password):
        self.conn = sqlite.connect(filename)
        self.c = self.conn.cursor()
        self.c.row_factory = sqlite.Row
        self.c.execute('PRAGMA key="' + password.replace('"', '\"') + '"')
        self.c.execute('PRAGMA kdf_iter = 24000')

    def generateKey(self, key, salt):
        # 2 Iterations of PBKDF2 SHA256
        return hashlib.pbkdf2_hmac('sha256', key, salt, 2)

    def getCryptoParams(self):
        ret = {}
        # Identity contains stuff to decrypt data columns
        try:
            self.c.execute("SELECT * FROM Identity")
        except sqlite.DatabaseError:
            print("Invalid password")
            sys.exit(1)

        identity = self.c.fetchone()

        # Info contains more parameters
        info = identity["Info"]

        # Get params from stream
        i = 16 # First 16 bytes are for "mHashData", which is unused
        ret["iv"] = bytearray()
        salt = bytearray()
        while i <= 31:
            ret["iv"].append(info[i])
            i += 1
        while i <= 47:
            salt.append(info[i])
            i += 1

        ret["iv"]  = bytes(ret["iv"])
        ret["key"] = self.generateKey(identity["Hash"].encode('utf-8'), salt)

        return ret

    def unpad(self, s):
        return s[0:-ord(s[-1])]

    def decrypt(self, enc, key, iv):
        # PKCS5
        cipher = AES.new(key, AES.MODE_CBC, iv)
        return self.unpad(str(cipher.decrypt(enc), 'utf-8'))

    def getCards(self, name):
        results = []
        name = name.lower()
        self.c.execute("SELECT * FROM Cards")
        cards = self.c.fetchall()
        with open(self.__getScriptPath() + '/.enpass', encoding='utf8', mode='w') as f:
            # File used for Bash/Zsh Command Completion...

            for card in cards:
                dec = self.decrypt(card["Data"], self.crypto["key"], self.crypto["iv"])
                card = json.loads(dec)

                if name == 'sudolikeaboss':
                    for field in sorted(card["fields"], key=lambda x:x['label']):
                        if field['type'] == 'url' and field['label'].lower() == 'location' and 'sudolikeaboss' in field['value']:
                            results.append(card)
                elif name == '*' and len(card["fields"]) > 0:
                    results.append(card)
                elif name in card["name"].lower() and len(card["fields"]) > 0:
                    results.append(card)

                f.write(card['name'].lower() + "\n")

        return results


def CardCompleter(prefix, **kwargs):
    # Bask/Zsh Command Completion...
    prefix = prefix.lower()
    return list(line for line in open(getScriptPath() + '/.enpass','r').read().splitlines() if line.startswith(prefix))


class PassCards:
    import sys

    def __init__(self, cardlist):
        self.carddata = {}

        for card in cardlist:

            self.carddata[card['uuid']] = {}
            self.carddata[card['uuid']]['name'] = card['name']

            for field in sorted(card["fields"], key=lambda x:x['label']):

                if field['isdeleted'] == 1:         # Skip if deleted...
                    continue

                if field['type'] == 'username' and field['value'] != '' \
                    and not 'username' in self.carddata[card['uuid']]:
                    self.carddata[card['uuid']]['username'] = field['value']

                elif field['type'] == 'email' and field['value'] != '' \
                    and not 'email' in self.carddata[card['uuid']]:
                    self.carddata[card['uuid']]['email']    = field['value']

                elif field['type'] == 'url' and field['value'] != '' \
                    and field['label'].lower() == 'website' and not 'website' in self.carddata[card['uuid']]:
                    self.carddata[card['uuid']]['website']  = field['value']

                elif field['type'] == 'url' and field['value'] != '' \
                    and field['label'].lower() == 'location' and not 'location' in self.carddata[card['uuid']]:
                     self.carddata[card['uuid']]['location'] = field['value']

                elif field['type'] == 'password' and field['isdeleted'] != 1 and field['value'] != '' \
                    and field['label'].lower() != 'passwordhistory' \
                    and not 'password' in self.carddata[card['uuid']]:
                    self.carddata[card['uuid']]['password'] = field['value']

                elif field['value'] != '':
                    if not 'additional' in self.carddata[card['uuid']]:
                        self.carddata[card['uuid']]['additional'] = []
                    self.carddata[card['uuid']]['additional'].append([field['label'], field['type'], field['value']])

            if card['note'] != '':
                self.carddata[card['uuid']]['note'] = card['note']


    def __pad(self, msg, length=12):
        return " "*2 + msg.ljust(length)

    def __warn(self, message):
        print(message, file=sys.stderr)

    def __quit(self, message):
        import sys
        print(message, file=sys.stderr)
        sys.exit(1)

    def copyToClip(self, message):
        if sys.platform == 'darwin':
            p = subprocess.Popen(['pbcopy'], stdin=subprocess.PIPE, close_fds=True)
            p.communicate(input=message.encode('utf-8'))
        elif sys.platform == 'linux':
            p = subprocess.Popen(['xclip', '-in', '-selection', 'clipboard'], stdin=subprocess.PIPE, close_fds=True)
            p.communicate(input=message.encode('utf-8'))
        else:
            self.__quit("No pasteboard integration for '" + sys.platform + "'; please consider using the 'print' command")

    def __getTermSize(self):
        """
        returns (lines:int, cols:int)
        """
        import os, struct
        def ioctl_GWINSZ(fd):
            import fcntl, termios
            return struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234"))

        # try stdin, stdout, stderr
        for fd in (0, 1, 2):
            try:
                return ioctl_GWINSZ(fd)
            except:
                pass

        # try os.ctermid()
        try:
            fd = os.open(os.ctermid(), os.O_RDONLY)
            try:
                return ioctl_GWINSZ(fd)
            finally:
                os.close(fd)
        except:
            pass

        # try `stty size`
        try:
            return tuple(int(x) for x in os.popen("stty size", "r").read().split())
        except:
            pass

        # try environment variables
        try:
            return tuple(int(os.getenv(var)) for var in ("LINES", "COLUMNS"))
        except:
            pass

        # i give up. return default.
        return (25, 80)

    def displayCards(self, alldata="no", passwords="no"):
        import re
        maxwrap = self.__getTermSize()[1] - len(pad('') + "  ")

        for uuid in sorted(self.carddata.keys()):
            print(self.carddata[uuid]['name'])
            print(len(self.carddata[uuid]['name'])*'-')
            if 'username' in self.carddata[uuid]: print(self.__pad("User Name") + ': ' + self.carddata[uuid]['username'])
            if 'email' in self.carddata[uuid]:    print(self.__pad("Email")     + ': ' + self.carddata[uuid]['email'])
            if 'website' in self.carddata[uuid]:  print(self.__pad("Website")   + ': ' + self.carddata[uuid]['website'])
            if 'location' in self.carddata[uuid]: print(self.__pad("Location ") + ': ' + self.carddata[uuid]['location'])

            if 'password' in self.carddata[uuid]:
                 print(self.__pad("Password")  + ': ', end='')
                 if passwords == "no":
                     print("Defined")
                 else:
                     print(self.carddata[uuid]['password'])

            if 'note' in self.carddata[uuid]:
                print("\n" + self.__pad("Note") + '| ' + \
                        ('\n' + pad('') +  '| ').join(line.strip() for \
                            line in re.findall(r'.{1,' + str(maxwrap) + '}(?:\s+|$)', self.carddata[uuid]['note'])))

            if alldata != 'no':
                if 'additional' in self.carddata[uuid]:
                    print("\n Additional Data\n")
                    for dset in (self.carddata[uuid]['additional']):
                        print(self.__pad(dset[0], 22) + ': ', end='')

                        if passwords == "no" and dset[1].lower() == 'password':
                            print("Defined")
                        else:
                            print(self.__pad(dset[2], 22) + "(" + dset[1] + ")")

            print()

    def selectCard(self):

        if len(self.carddata) > 1:
            choices = {}

            for uuid in sorted(self.carddata.keys()):
                if 'username' in self.carddata[uuid]:
                    choices[self.carddata[uuid]['name'] + " - " + self.carddata[uuid]['username']] = uuid
                else:
                    choices[self.carddata[uuid]['name']] = uuid

            chooser = Chooser(choices.keys())
            selection = chooser.choice()
            try:
                return choices[selection]
            except KeyError:
                self.__quit("No password defined for '" + selection + "'")

        else:
            return list(self.carddata)[0]

    def processCard(self, entry, command):

        if command == 'copy' or command == 'quietcopy':
            try:
                self.copyToClip(self.carddata[entry]['password'])
                if command == 'copy':
                    print("Copied password for '" + self.carddata[entry]['name'] + "' to the pasteboard")
            except KeyError:
                self.__quit("No password defined for '" + self.carddata[entry]['name'] + "'")

        else:
            print(self.carddata[entry]['password'])


def main(argv=None):
    import sys
    global wallet

    command = ''
    name    = ''

    alldata  = 'no'
    showpass = 'no'

    if argv is None:
        parser = argparse.ArgumentParser ()
        parser.add_argument('-q', '--quiet', action='store_true', help='Supress Standard Output Notifications')

        parser.add_argument('-w', '--wallet', help='The Enpass wallet file')

        parser.add_argument('-a', '--alldata', action='store_true', help='Displays all of the known data in of each card')
        parser.add_argument('--please_show_me_the_passwords', action='store_true', help='Display passwords where present')

        parser.add_argument("command", choices=('get', 'list', 'copy', 'print'), help="Show entry, copy or print password")
        parser.add_argument("name", help="The entry name, use '*' to see all").completer = CardCompleter

        argcomplete.autocomplete(parser)
        args = parser.parse_args()

        command = args.command
        name = args.name

        if args.wallet is not None:
            wallet = args.wallet
        if args.alldata is True:
            alldata = 'yes'
        if args.please_show_me_the_passwords is True:
            showpass = 'yes'
        if args.quiet is True and command == 'copy':
            command = 'quietcopy'
        if command == 'print' and showpass != 'yes':
            abort("Please verify the printing of the passwords using '--please_show_me_the_passwords'")

    else:
        if len(argv) != 3:
            abort("Args: command wallet name")

        command = argv[0]
        if not wallet:
            wallet = argv[1]

        name = argv[2]

    if (args.command is None or args.command not in ['copy','get','list','print','quietcopy']):
        abort("Command: get, list, copy, print")

    if not os.path.isfile(wallet):
        abort("Wallet not found: " + wallet)

    password = keyring.get_password('enpass', 'enpass')
    if password is None:
        password = getpass.getpass("Master Password: ")
        if os.path.isfile(password_store_decline):
            pass
        else:
            response = input('Would you like to save your master password in the keyring? (Y/n)').lower()
            if response == 'y' or response == '':
                keyring.set_password('enpass', 'enpass', str(password))
            else:
                open(password_store_decline, 'w')

    en = Enpassant(wallet, str(password))
    cards = en.getCards(name)

    if len(cards) == 0:
        abort("No entries for '" + name + "'")

    carddata = PassCards(cards)

    if command == "list" or command == "get":
        carddata.displayCards(alldata, showpass)
    else:
        carddata.processCard(carddata.selectCard(), command)

if __name__ == "__main__":
    exit(main())