"""
Password generation for the Jupyter Server.
"""

from contextlib import contextmanager
import getpass
import hashlib
import io
import json
import os
import random
import traceback
import warnings

import argon2
import argon2.exceptions
from argon2 import PasswordHasher
from ipython_genutils.py3compat import cast_bytes, str_to_bytes, cast_unicode
from traitlets.config import Config, ConfigFileNotFound, JSONFileConfigLoader
from jupyter_core.paths import jupyter_config_dir

# Length of the salt in nr of hex chars, which implies salt_len * 4
# bits of randomness.
salt_len = 12


def passwd(passphrase=None, algorithm='argon2'):
    """Generate hashed password and salt for use in server configuration.

    In the server configuration, set `c.ServerApp.password` to
    the generated string.

    Parameters
    ----------
    passphrase : str
        Password to hash.  If unspecified, the user is asked to input
        and verify a password.
    algorithm : str
        Hashing algorithm to use (e.g, 'sha1' or any argument supported
        by :func:`hashlib.new`, or 'argon2').

    Returns
    -------
    hashed_passphrase : str
        Hashed password, in the format 'hash_algorithm:salt:passphrase_hash'.

    Examples
    --------
    >>> passwd('mypassword')
    'sha1:7cf3:b7d6da294ea9592a9480c8f52e63cd42cfb9dd12'

    """
    if passphrase is None:
        for i in range(3):
            p0 = getpass.getpass('Enter password: ')
            p1 = getpass.getpass('Verify password: ')
            if p0 == p1:
                passphrase = p0
                break
            else:
                print('Passwords do not match.')
        else:
            raise ValueError('No matching passwords found. Giving up.')

    if algorithm == 'argon2':
        ph = PasswordHasher(
            memory_cost=10240,
            time_cost=10,
            parallelism=8,
        )
        h = ph.hash(passphrase)

        return ':'.join((algorithm, cast_unicode(h, 'ascii')))

    h = hashlib.new(algorithm)
    salt = ('%0' + str(salt_len) + 'x') % random.getrandbits(4 * salt_len)
    h.update(cast_bytes(passphrase, 'utf-8') + str_to_bytes(salt, 'ascii'))

    return ':'.join((algorithm, salt, h.hexdigest()))


def passwd_check(hashed_passphrase, passphrase):
    """Verify that a given passphrase matches its hashed version.

    Parameters
    ----------
    hashed_passphrase : str
        Hashed password, in the format returned by `passwd`.
    passphrase : str
        Passphrase to validate.

    Returns
    -------
    valid : bool
        True if the passphrase matches the hash.

    Examples
    --------
    >>> from jupyter_server.auth.security import passwd_check
    >>> passwd_check('argon2:...', 'mypassword')
    True

    >>> passwd_check('argon2:...', 'otherpassword')
    False

    >>> passwd_check('sha1:0e112c3ddfce:a68df677475c2b47b6e86d0467eec97ac5f4b85a',
    ...              'mypassword')
    True
    """
    if hashed_passphrase.startswith('argon2:'):
        ph = argon2.PasswordHasher()

        try:
            return ph.verify(hashed_passphrase[7:], passphrase)
        except argon2.exceptions.VerificationError:
            return False

    try:
        algorithm, salt, pw_digest = hashed_passphrase.split(':', 2)
    except (ValueError, TypeError):
        return False

    try:
        h = hashlib.new(algorithm)
    except ValueError:
        return False

    if len(pw_digest) == 0:
        return False

    h.update(cast_bytes(passphrase, 'utf-8') + cast_bytes(salt, 'ascii'))

    return h.hexdigest() == pw_digest

@contextmanager
def persist_config(config_file=None, mode=0o600):
    """Context manager that can be used to modify a config object

    On exit of the context manager, the config will be written back to disk,
    by default with user-only (600) permissions.
    """

    if config_file is None:
        config_file = os.path.join(jupyter_config_dir(), 'jupyter_server_config.json')

    os.makedirs(os.path.dirname(config_file), exist_ok=True)

    loader = JSONFileConfigLoader(os.path.basename(config_file), os.path.dirname(config_file))
    try:
        config = loader.load_config()
    except ConfigFileNotFound:
        config = Config()

    yield config

    with io.open(config_file, 'w', encoding='utf8') as f:
        f.write(cast_unicode(json.dumps(config, indent=2)))

    try:
        os.chmod(config_file, mode)
    except Exception as e:
        tb = traceback.format_exc()
        warnings.warn("Failed to set permissions on %s:\n%s" % (config_file, tb),
            RuntimeWarning)

def set_password(password=None, config_file=None):
    """Ask user for password, store it in JSON configuration file"""

    hashed_password = passwd(password)

    with persist_config(config_file) as config:
        config.ServerApp.password = hashed_password
