import base64
import datetime
import hashlib
import logging
import typing

import bcrypt

from flask import abort, Blueprint, current_app, jsonify, request
from pillar.api.utils.authentication import create_new_user_document
from pillar.api.utils.authentication import make_unique_username
from pillar.api.utils.authentication import store_token
from pillar.api.utils import utcnow

blueprint = Blueprint('authentication', __name__)
log = logging.getLogger(__name__)


def get_auth_credentials(user, provider):
    return next((credentials for credentials in user['auth'] if 'provider'
                 in credentials and credentials['provider'] == provider), None)


def create_local_user(email, password):
    """For internal user only. Given username and password, create a user."""
    # Hash the password
    hashed_password = hash_password(password, bcrypt.gensalt())
    db_user = create_new_user_document(email, '', email, provider='local',
                                       token=hashed_password)
    # Make username unique
    db_user['username'] = make_unique_username(email)
    # Create the user
    r, _, _, status = current_app.post_internal('users', db_user)
    if status != 201:
        log.error('internal response: %r %r', status, r)
        return abort(500)
    # Return user ID
    return r['_id']


def get_local_user(username, password):
    # Look up user in db
    users_collection = current_app.data.driver.db['users']
    user = users_collection.find_one({'username': username})
    if not user:
        return abort(403)
    # Check if user has "local" auth type
    credentials = get_auth_credentials(user, 'local')
    if not credentials:
        return abort(403)
    # Verify password
    salt = credentials['token']
    hashed_password = hash_password(password, salt)
    if hashed_password != credentials['token']:
        return abort(403)
    return user


@blueprint.route('/make-token', methods=['POST'])
def make_token():
    """Direct login for a user, without OAuth, using local database. Generates
    a token that is passed back to Pillar Web and used in subsequent
    transactions.

    :return: a token string
    """
    username = request.form['username']
    password = request.form['password']

    user = get_local_user(username, password)

    token = generate_and_store_token(user['_id'])
    return jsonify(token=token['token'])


def generate_and_store_token(user_id, days=15, prefix=b'') -> dict:
    """Generates token based on random bits.

    NOTE: the returned document includes the plain-text token.
    DO NOT STORE OR LOG THIS unless there is a good reason to.

    :param user_id: ObjectId of the owning user.
    :param days: token will expire in this many days.
    :param prefix: the token will be prefixed by these bytes, for easy identification.
    :return: the token document with the token in plain text as well as hashed.
    """

    if not isinstance(prefix, bytes):
        raise TypeError('prefix must be bytes, not %s' % type(prefix))

    import secrets

    random_bits = secrets.token_bytes(32)

    # Use 'xy' as altargs to prevent + and / characters from appearing.
    # We never have to b64decode the string anyway.
    token = prefix + base64.b64encode(random_bits, altchars=b'xy').strip(b'=')

    token_expiry = utcnow() + datetime.timedelta(days=days)
    return store_token(user_id, token.decode('ascii'), token_expiry)


def hash_password(password: str, salt: typing.Union[str, bytes]) -> str:
    password = password.encode()

    if isinstance(salt, str):
        salt = salt.encode('utf-8')

    hash = hashlib.sha256(password).digest()
    encoded_password = base64.b64encode(hash)
    hashed_password = bcrypt.hashpw(encoded_password, salt)
    return hashed_password.decode('ascii')


def setup_app(app, url_prefix):
    app.register_api_blueprint(blueprint, url_prefix=url_prefix)
