# coding=utf-8
"""
itb_security
============
"""

__all__ = [
    'check_password_security',
    'create_key',
    'create_secure_key',
]

from os import urandom
from secrets import choice
from string import ascii_lowercase as LOWERCASE
from string import ascii_uppercase as UPPERCASE
from string import digits as DIGITS
from string import printable as PRINTABLE
from string import whitespace as WHITESPACE
from typing import List, Literal, Optional


def create_secure_key(
    bits: int = 256,
    max_len: Optional[int] = None,
    key: Optional[str] = None,
) -> str:
    """Generate a cryptographically secure hex key

    :param bits: key strength in bits (default 256)
    :param max_len: optional maximum length of the returned string
    :param key: if provided, this key is used instead of generating a new one
    :return: hex key string
    """

    if not key:
        if bits % 8 != 0:
            raise ValueError(f'bits must be a multiple of 8, got {bits}')
        key = urandom(bits // 8).hex()

    if max_len and len(key) > max_len:
        key = key[:max_len]

    return key


def create_key(
    keyLength: int = 15,
    mode: Literal['alpha', 'alphaLower', 'alphaUpper', 'alphaNum', 'alphaNumLower', 'alphaNumUpper', 'lowerChars', 'upperChars', 'numbers', 'all'] = 'alphaNum'
) -> str:
    """Generate a random key from a character set determined by mode

    :param keyLength: length of the generated key
    :param mode: character set to use:

        - ``'alpha'``: lowercase + uppercase letters
        - ``'alphaLower'``: lowercase letters only (alias for ``lowerChars``)
        - ``'alphaUpper'``: uppercase letters only (alias for ``upperChars``)
        - ``'alphaNum'`` (default): lowercase + uppercase + digits
        - ``'alphaNumLower'``: lowercase + digits
        - ``'alphaNumUpper'``: uppercase + digits
        - ``'lowerChars'``: lowercase letters only
        - ``'upperChars'``: uppercase letters only
        - ``'numbers'``: digits only
        - ``'all'``: all printable characters except whitespace and ``^``

    :return: randomly generated key string
    """
    elements = ''

    if mode == 'alpha':
        elements = LOWERCASE + UPPERCASE
    elif mode in ('alphaLower', 'lowerChars'):
        elements = LOWERCASE
    elif mode in ('alphaUpper', 'upperChars'):
        elements = UPPERCASE
    elif mode == 'alphaNum':
        elements = LOWERCASE + UPPERCASE + DIGITS
    elif mode == 'alphaNumLower':
        elements = LOWERCASE + DIGITS
    elif mode == 'alphaNumUpper':
        elements = UPPERCASE + DIGITS
    elif mode == 'numbers':
        elements = DIGITS
    elif mode == 'all':
        elements = ''.join(set(PRINTABLE) - set(WHITESPACE) - {'^'})

    key = ''.join([choice(elements) for _ in range(keyLength)])

    return key


def check_password_security(
    pw: str,
    length: int = 8,
    check_ascii: bool = True,
    check_nonalphanum: bool = False,
    check_number: bool = False,
    check_capital: bool = False
) -> List[str]:
    """Check if a password meets certain security criteria

    :param pw: the password to check
    :param length: minimum length
    :param check_ascii: require only ASCII characters (for keyboard compatibility)
    :param check_nonalphanum: require at least one non-alphanumeric character
    :param check_number: require at least one digit
    :param check_capital: require at least one uppercase letter
    :return: list of error messages (empty if all checks pass)
    """

    errors = []

    if len(pw) < length:
        errors.append(
            f'The minimum password length should be {length}'
        )

    if check_ascii and not pw.isascii():
        errors.append(
            'Only ASCII characters are allowed in the password'
        )

    if check_number and not any(c.isdigit() for c in pw):
        errors.append(
            'The password should contain at least one digit'
        )

    if check_nonalphanum and not any(not c.isalnum() for c in pw):
        errors.append(
            'The password should contain at least one non alpha-numeric character'
        )

    if check_capital and not any(c.isupper() for c in pw):
        errors.append(
            'The password should contain at least one uppercase character'
        )

    return errors
