#
# Requirements: argon2-cffi
#
import argon2
import argparse
import hashlib
import re

from . import bhash, phogen

#
# WARNING: Changing any of the parameters below will affect password generation
#
PASSGENY_DEFAULT_PATTERN = "^6p^6p^6ps2p100d"

# List of special characters
PASSGENY_SPECIAL =  "-./=_ "

# Default SALT to use; this cannot be random since passgeny must
# generate predictable passwords
PASSGENY_SALT_DEFAULT = b"Passgeny v1"
# Memory cost expresssed in kb
PASSGENY_ARGON2_MEMORY_COST = 8192
# Number of iterations
PASSGENY_ARGON2_TIME_COST = 6
# Number of parallel threads
PASSGENY_ARGON2_PARALLEL = 4
# Hash length - 512 bits by default
PASSGENY_ARGON2_HASH_LEN = 128

class PassgenyInvalidPattern(Exception):
    pass

class Passgeny:
    def __init__(self, master_password):
        self.master_password = hashlib.sha256(master_password.encode()).digest()
        self.pattern = PASSGENY_DEFAULT_PATTERN

    def generate(self, domain, user, *tokens):
        bh = bhash.Bhash()
        bh.from_bytes(self.__hash([domain, user, *tokens]))

        return self.__generate_from_pattern(bh)

    def set_pattern(self, pattern):
        self.pattern = pattern

    def __argon2_hash(self, message):
        return argon2.low_level.hash_secret_raw(
                message,
                PASSGENY_SALT_DEFAULT,
                type = argon2.Type.ID,
                memory_cost = PASSGENY_ARGON2_MEMORY_COST,
                time_cost = PASSGENY_ARGON2_TIME_COST,
                parallelism = PASSGENY_ARGON2_PARALLEL,
                hash_len = PASSGENY_ARGON2_HASH_LEN)

    def __hash(self, token_list):
        message = b''
        # Construct the message to be hashed
        for x in token_list:
            message += x.encode() + b'\0'

        # Append the SHA256 of the master password
        message += self.master_password

        # Compute the hash using argon2
        return self.__argon2_hash(message)

    def __generate_from_pattern(self, bh):
        pattern = self.pattern
        flags = ""
        password = ""

        def gen_phogen(match):
            plen = 1 if match[1] == "" else int(match[1])
            return phogen.encode(bh, plen)

        def gen_special(match):
            slen = 1 if match[1] == "" else int(match[1])
            return PASSGENY_SPECIAL[bh.modulo(len(PASSGENY_SPECIAL))]

        def gen_decimal(match):
            dlen = 1 if match[1] == "" else int(match[1])
            return "{}".format(bh.modulo(dlen))

        def gen_hexstr(match):
            x = ""
            for l in range(int(match[1])):
                x += "{:x}".format(bh.modulo(16))

            return x


        def gen_capitalize(match):
            nonlocal flags
            flags += '^'
            return None

        pattern_list = [
                [ '([0-9]*)p', gen_phogen ],
                [ '([0-9]*)s', gen_special ],
                [ '([0-9]+)d', gen_decimal ],
                [ '([0-9]+)x', gen_hexstr ],
                [ '\^', gen_capitalize ],
        ]

        while pattern:
            pw = None

            for pm in pattern_list:
                if match := re.match(pm[0], pattern):
                    break

            if not match:
                raise PassgenyInvalidPattern("Invalid pattern: {}".format(self.pattern))

            pw = pm[1](match)
            pattern = pattern[len(match[0]):]

            if not pw: continue

            if "^" in flags:
                pw = pw.capitalize()

            password += pw
            flags = ""

        return password

