import logging
import jwt
import hug
import base64
import binascii
from ldap3 import SIMPLE, Server, Connection, ALL
from peewee import DoesNotExist, DatabaseError
from falcon import HTTPUnauthorized

from db.directives import PeeweeContext, PeeweeSession
from db.model import User
from config import config
from secret_token.secret_token import hash_pw

log = logging.getLogger('auth')


def token_verify(token, context: PeeweeContext):
    secret = config.Settings.jwt_key
    try:
        user_object = jwt.decode(token, secret, algorithms=["HS256"])
        return get_user(user_object['user'], context)
    except jwt.DecodeError as error:
        log.warning("decodeError {}".format(error))
        return False


token_key_authentication = hug.authentication.token(token_verify)


class UserRoles:
    ADMIN = 'admin'
    USER = 'doctor'
    ANON = 'anonymous'

    @staticmethod
    def user_roles():
        return [UserRoles.ADMIN, UserRoles.USER, UserRoles.ANON]


def normalize_user(user_name):
    return user_name.lower()


def get_user(user_name: str, context: PeeweeContext):
    with context.db.atomic():
        try:
            return User.get(User.user_name == user_name)
        except DoesNotExist:
            log.warning("user not found: %s", user_name)
            return False
        except DatabaseError:
            log.exception("unknown error logging in: %s", user_name)
            return False


def verify_user(user_name, user_password, context: PeeweeContext):
    name = normalize_user(user_name)
    with context.db.atomic():
        try:
            user = User.get(User.user_name == name)
            if user.role == UserRoles.ANON:
                return user
            salt = user.salt
            hashed = hash_pw(name, salt, user_password)
            if hashed == user.password:
                return user
            log.warning("invalid credentials for user: %s", user_name)
            return False
        except DoesNotExist:
            if config.Settings.use_ldap:
                return search_ldap_user(user_name, user_password, context)
            else:
                log.warning("user not found: %s", user_name)
                return False
        except DatabaseError:
            log.exception("unknown error logging in: %s", user_name)
            return False


def search_ldap_user(user_name: str, user_password: str, context: PeeweeContext):
    url = config.Ldap.url
    sys_user = config.Ldap.user_dn
    sys_pw = config.Ldap.user_pw
    base = config.Ldap.search_base
    filter = config.Ldap.search_filter
    attribute = config.Ldap.search_attribute
    if "" in [url, sys_user, sys_pw, base, filter, attribute]:
        log.error(
            "ENV variables for LDAP not set. You'll need to define LDAP_URL, LDAP_SYSTEM_DN, LDAP_SYSTEM_USER_PW, LDAP_SEARCH_BASE, LDAP_SEARCH_FILTER, LDAP_ATTRIBUTE")
        return False

    with_tls = config.Ldap.use_tls
    port = config.Ldap.tls_port if with_tls else config.Ldap.port
    server = Server(url, port=port, use_ssl=with_tls,
                    get_info=ALL)

    # Connects the system user
    connection = Connection(
        server, sys_user, sys_pw)
    result = connection.bind()
    if with_tls:
        connection.start_tls()
    if result:
        # searches for the user about to log in in the ldap server
        connection.search(base, filter.format(
            user_name), attributes=[attribute])
        # if exactly one was found, tries to log this one in
        if len(connection.entries) == 1:
            userConnection = Connection(
                server, user=connection.entries[0].entry_dn, password=<PASSWORD>, authentication=SIMPLE)
            isValid = userConnection.bind()
            if isValid:
                # creates a user if not existing yet, in order to track coupon numbers per ldap user
                return get_or_create_auto_user(context.db, UserRoles.USER, f'ldap-{user_name}')
    log.warning(f"Didn't find an ldap user for uid {user_name}")
    return False


def get_or_create_anon_user(context: PeeweeContext):
    name = "unregistered_user"
    return get_or_create_auto_user(context.db, UserRoles.ANON, name)


def get_or_create_auto_user(db: PeeweeSession, role: str, name: str):
    coupons = 4 if (
        role == UserRoles.ANON) else config.Ldap.user_coupon_number if role == UserRoles.USER else 1
    with db.atomic():
        try:
            user = User.get(User.user_name == name)
            return user
        except DoesNotExist:
            user = User.create(user_name=name, role=role,
                               salt="", password="", coupons=coupons)
            user.save()
            return user


@ hug.authentication.authenticator
def basic(request, response, own_verify_user, realm="simple", context=None, **kwargs):
    """Basic HTTP Authentication"""
    http_auth = request.auth
    response.set_header("WWW-Authenticate", "Basic")

    if http_auth is None:
        if config.Settings.disable_auth_for_booking:
            user = get_or_create_anon_user(context)
            return user
        return

    if isinstance(http_auth, bytes):
        http_auth = http_auth.decode("utf8")
    try:
        auth_type, user_and_key = http_auth.split(" ", 1)
    except ValueError:
        raise HTTPUnauthorized(
            "Authentication Error",
            "Authentication header is improperly formed",
            challenges=('Basic realm="{}"'.format(realm),),
        )
    if auth_type.lower() == "basic":
        try:
            user_id, key = (
                base64.decodebytes(bytes(user_and_key.strip(), "utf8")).decode(
                    "utf8").split(":", 1)
            )
            user = own_verify_user(user_id, key, context)
            if user:
                response.set_header("WWW-Authenticate", "")
                return user
        except (binascii.Error, ValueError):
            raise HTTPUnauthorized(
                "Authentication Error",
                "Unable to determine user and password with provided encoding",
                challenges=('Basic realm="{}"'.format(realm),),
            )
    return False


@ basic
def switchable_authentication(user_name, user_password, context: PeeweeContext):
    return verify_user(user_name, user_password, context)


@ hug.authentication.basic
def authentication(user_name, user_password, context: PeeweeContext):
    user = verify_user(user_name, user_password, context)
    if user and user.role == UserRoles.ANON:
        return False
    return user


@ hug.authentication.basic
def admin_authentication(user_name, user_password, context: PeeweeContext):
    user = verify_user(user_name, user_password, context)
    if user and user.role == UserRoles.ADMIN:
        return user
    log.warning("missing admin role for: %s", user_name)
    return False

import logging
import jwt
import hug
import base64
import binascii
from ldap3 import SIMPLE, Server, Connection, ALL
from peewee import DoesNotExist, DatabaseError
from falcon import HTTPUnauthorized

from db.directives import PeeweeContext, PeeweeSession
from db.model import User
from config import config
from secret_token.secret_token import hash_pw

log = logging.getLogger('auth')


def token_verify(token, context: PeeweeContext):
    secret = config.Settings.jwt_key
    try:
        user_object = jwt.decode(token, secret, algorithms=["HS256"])
        return get_user(user_object['user'], context)
    except jwt.DecodeError as error:
        log.warning("decodeError {}".format(error))
        return False


token_key_authentication = hug.authentication.token(token_verify)


class UserRoles:
    ADMIN = 'admin'
    USER = 'doctor'
    ANON = 'anonymous'

    @staticmethod
    def user_roles():
        return [UserRoles.ADMIN, UserRoles.USER, UserRoles.ANON]


def normalize_user(user_name):
    return user_name.lower()


def get_user(user_name: str, context: PeeweeContext):
    with context.db.atomic():
        try:
            return User.get(User.user_name == user_name)
        except DoesNotExist:
            log.warning("user not found: %s", user_name)
            return False
        except DatabaseError:
            log.exception("unknown error logging in: %s", user_name)
            return False


def verify_user(user_name, user_password, context: PeeweeContext):
    name = normalize_user(user_name)
    with context.db.atomic():
        try:
            user = User.get(User.user_name == name)
            if user.role == UserRoles.ANON:
                return user
            salt = user.salt
            hashed = hash_pw(name, salt, user_password)
            if hashed == user.password:
                return user
            log.warning("invalid credentials for user: %s", user_name)
            return False
        except DoesNotExist:
            if config.Settings.use_ldap:
                return search_ldap_user(user_name, user_password, context)
            else:
                log.warning("user not found: %s", user_name)
                return False
        except DatabaseError:
            log.exception("unknown error logging in: %s", user_name)
            return False


def search_ldap_user(user_name: str, user_password: str, context: PeeweeContext):
    url = config.Ldap.url
    sys_user = config.Ldap.user_dn
    sys_pw = config.Ldap.user_pw
    base = config.Ldap.search_base
    filter = config.Ldap.search_filter
    attribute = config.Ldap.search_attribute
    if "" in [url, sys_user, sys_pw, base, filter, attribute]:
        log.error(
            "ENV variables for LDAP not set. You'll need to define LDAP_URL, LDAP_SYSTEM_DN, LDAP_SYSTEM_USER_PW, LDAP_SEARCH_BASE, LDAP_SEARCH_FILTER, LDAP_ATTRIBUTE")
        return False

    with_tls = config.Ldap.use_tls
    port = config.Ldap.tls_port if with_tls else config.Ldap.port
    server = Server(url, port=port, use_ssl=with_tls,
                    get_info=ALL)

    # Connects the system user
    connection = Connection(
        server, sys_user, sys_pw)
    result = connection.bind()
    if with_tls:
        connection.start_tls()
    if result:
        # searches for the user about to log in in the ldap server
        connection.search(base, filter.format(
            user_name), attributes=[attribute])
        # if exactly one was found, tries to log this one in
        if len(connection.entries) == 1:
            userConnection = Connection(
                server, user=connection.entries[0].entry_dn, password=<PASSWORD>, authentication=SIMPLE)
            isValid = userConnection.bind()
            if isValid:
                # creates a user if not existing yet, in order to track coupon numbers per ldap user
                return get_or_create_auto_user(context.db, UserRoles.USER, f'ldap-{user_name}')
    log.warning(f"Didn't find an ldap user for uid {user_name}")
    return False


def get_or_create_anon_user(context: PeeweeContext):
    name = "unregistered_user"
    return get_or_create_auto_user(context.db, UserRoles.ANON, name)


def get_or_create_auto_user(db: PeeweeSession, role: str, name: str):
    coupons = 4 if (
        role == UserRoles.ANON) else config.Ldap.user_coupon_number if role == UserRoles.USER else 1
    with db.atomic():
        try:
            user = User.get(User.user_name == name)
            return user
        except DoesNotExist:
            user = User.create(user_name=name, role=role,
                               salt="", password="", coupons=coupons)
            user.save()
            return user


@ hug.authentication.authenticator
def basic(request, response, own_verify_user, realm="simple", context=None, **kwargs):
    """Basic HTTP Authentication"""
    http_auth = request.auth
    response.set_header("WWW-Authenticate", "Basic")

    if http_auth is None:
        if config.Settings.disable_auth_for_booking:
            user = get_or_create_anon_user(context)
            return user
        return

    if isinstance(http_auth, bytes):
        http_auth = http_auth.decode("utf8")
    try:
        auth_type, user_and_key = http_auth.split(" ", 1)
    except ValueError:
        raise HTTPUnauthorized(
            "Authentication Error",
            "Authentication header is improperly formed",
            challenges=('Basic realm="{}"'.format(realm),),
        )
    if auth_type.lower() == "basic":
        try:
            user_id, key = (
                base64.decodebytes(bytes(user_and_key.strip(), "utf8")).decode(
                    "utf8").split(":", 1)
            )
            user = own_verify_user(user_id, key, context)
            if user:
                response.set_header("WWW-Authenticate", "")
                return user
        except (binascii.Error, ValueError):
            raise HTTPUnauthorized(
                "Authentication Error",
                "Unable to determine user and password with provided encoding",
                challenges=('Basic realm="{}"'.format(realm),),
            )
    return False


@ basic
def switchable_authentication(user_name, user_password, context: PeeweeContext):
    return verify_user(user_name, user_password, context)


@ hug.authentication.basic
def authentication(user_name, user_password, context: PeeweeContext):
    user = verify_user(user_name, user_password, context)
    if user and user.role == UserRoles.ANON:
        return False
    return user


@ hug.authentication.basic
def admin_authentication(user_name, user_password, context: PeeweeContext):
    user = verify_user(user_name, user_password, context)
    if user and user.role == UserRoles.ADMIN:
        return user
    log.warning("missing admin role for: %s", user_name)
    return False

# Connects the system user # searches for the user about to log in in the ldap server # if exactly one was found, tries to log this one in # creates a user if not existing yet, in order to track coupon numbers per ldap user Basic HTTP Authentication