import json
from functools import wraps

from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
from lib.helpers import JSONEncoder
from peewee import AutoField, TextField
from playhouse.sqlite_ext import JSONField

from .base_model import BaseModel, db
from .log import Log

hasher = PasswordHasher()

current_session = None  # THIS SHOULD NEVER BE IMPORTED
# if imported it will not change unless you re-import it
# USE Users.get_session() INSTEAD.
# also this is not thread safe


class Session:
    def __init__(self, user):
        if type(user) is not Users:
            raise AssertionError(f"user is wrong type: {type(user)} instead of {Users}")
        self.user = user
        self.username = self.user.username
        self.roles = Users.parse_roles(self.user.roles)
        self.is_admin = self.user.is_admin


def json_dumps_roles(roles):
    return json.dumps(roles, cls=JSONEncoder)


def json_loads_roles(roles):
    return Users.parse_roles(json.loads(roles))


class Users(BaseModel):
    id = AutoField(primary_key=True, unique=True, null=False)
    username = TextField(unique=True, null=False)
    password = TextField(null=False)  # enable nulling to enable passwordless login
    roles = JSONField(null=True, json_dumps=json_dumps_roles, json_loads=json_loads_roles)

    @classmethod
    def get_users(cls):
        return cls.select().where(cls.password != b"").execute()

    @classmethod
    def get_usernames(cls):
        return sorted(set([str(user.username) for user in cls.get_users()]))

    @classmethod
    def get_user(cls, username):
        return cls.get_or_none(cls.username == username, cls.password != b"")

    @classmethod
    def generate(cls, username, password, roles=None):
        if username is None or not len(username):
            raise AssertionError("Il nome utente non può essere vuoto")
        if password is None or not len(password):
            raise AssertionError("La password non può essere vuota")
        if any(map(lambda x: x == "\u2022", password)):
            raise AssertionError("La password non può contenere il carattere proibito: \u2022")
        return dict(username=username, password=hasher.hash(password), roles=list(cls.parse_roles(roles)))

    @classmethod
    @db.atomic()
    def register(cls, username, password, roles=None):
        generated_user = cls.generate(username=username, password=password, roles=roles)
        cls.replace(**generated_user).execute()
        Log.log("Sign up", "username: \"{}\", roles \"{}\"".format(username, roles))

    @classmethod
    def parse_roles(cls, roles=None):
        if roles is None:
            return set()
        if type(roles) is str:
            roles = set(map(lambda role: role.strip(" \n\r\t\v,").lower(), roles.split(",")))
        roles = set(map(lambda role: role.lower(), roles))
        roles.discard(None)
        roles.discard("")
        return roles

    @classmethod
    @db.atomic()
    def login(cls, username, password):
        Log.log("Login", f"Login attempt: username: {username!r}")
        user = cls.get_user(username)
        if user is None:
            Log.log("Login", f"username: {username!r}: BAD username")
            return False
        if user.verify(password):
            Log.log("Login", f"username: {user.username!r}: SUCCESSFUL, roles: {user.roles!r}")
            global current_session
            current_session = Session(user)
            return current_session
        else:
            Log.log("Login", f"username: {user.username!r}, password {password!r}: BAD password")
            return False

    @classmethod
    @db.atomic()
    def logout(cls):
        global current_session
        if current_session is None:
            Log.log("Logout", "Already logged out")
            return
        Log.log("Logout", f"Logout: username: {current_session.username!r}")
        current_session = None
        return current_session

    @classmethod
    def get_session(cls):
        global current_session
        return current_session

    @db.atomic()
    def verify(self, password):
        if self.password is None:
            return True
        if self.password == "":
            return False
        try:
            hasher.verify(self.password, password)
        except VerifyMismatchError:
            return False
        if hasher.check_needs_rehash(self.password):
            self.password = hasher.hash(password)
            self.save()
        return True

    @classmethod
    @db.atomic()
    def delete_by_username(cls, username):
        cls.update(password="").where(cls.username == username).execute()

    @classmethod
    def delete(cls, *args, **kwargs):
        # OVERRIDE DELETION
        # so that deleting a user will only disable it
        return cls.update(password="")

    @property
    def is_admin(self):
        try:
            return "admin" in self.parse_roles(self.roles)
        except Exception:
            return False

    class Meta:
        table_name = "user"


def admin_login_required(func):
    @wraps(func)
    def function(*args, **kwargs):
        session = Users.get_session()
        if session is None or not session.is_admin:
            raise RuntimeError("method not authorized for non superusers")
        return func(*args, **kwargs)
    return function
