# -*- coding: utf8 -*-
import re

from flask_login import AnonymousUserMixin
from werkzeug.security import check_password_hash, generate_password_hash

from ...utils import NOTIF_ERROR, NOTIF_OK, NOTIF_WARN, __
from .interface import BUIhandler, BUIloader, BUIuser


class BasicLoader(BUIloader):
    """The :class:`burpui.misc.auth.basic.BasicLoader` class loads the *Basic*
    users.
    """

    section = name = "BASIC:AUTH"

    def __init__(self, app=None, handler=None):
        """:func:`burpui.misc.auth.basic.BasicLoader.__init__` loads users from
        the configuration file.

        :param app: Instance of the app we are running in
        :type app: :class:`burpui.engines.server.BUIServer`

        :param handler: Instance of handler
        :type handler: :class:`burpui.misc.auth.interface.BUIhandler`
        """
        self.app = app
        self.conf = self.app.conf
        self.conf_id = None
        self._users = {}
        self.handler = handler
        self.handler.name = self.name
        self.handler.add_user = self.add_user
        self.handler.del_user = self.del_user
        self.handler.change_password = self.change_password
        self.load_users(True)

    def load_users(self, force=False):
        if not force and self.conf_id:
            if not self.conf.changed(self.conf_id):
                return False

        self._users = {
            "admin": {"pwd": generate_password_hash("admin"), "salted": True}
        }

        if self.section in self.conf.options:
            # check passwords are salted
            salted = False
            changed = False
            # This is not necessary for now. Maybe will use this some day
            # TODO: clean this?
            # if len(self.conf.options.comments[self.section]) > 0:
            #     if re.match(
            #             r'^\s*#+\s*@salted@',
            #             self.conf.options.comments[self.section][-1]):
            #         salted = True
            # allow mixed logins (plain and hashed)
            mixed = self.conf.safe_get(
                "mixed", cast="boolean", section=self.section, defaults=False
            )
            self._users = {}
            for opt in self.conf.options.get(self.section).keys():
                salt = True
                if opt == "priority":
                    # Maybe the handler argument is None, maybe the 'priority'
                    # option is missing. We don't care.
                    try:
                        self.handler.priority = (
                            self.conf.safe_get(opt, "integer", section=self.section)
                            or self.handler.priority
                        )
                    except:
                        pass
                    continue  # pragma: no cover
                # list of reserved options
                if opt in ["mixed"]:
                    continue
                pwd = self.conf.safe_get(opt, section=self.section)
                if not salted or mixed:
                    if not re.match(r"^pbkdf2:.+\$.+\$.+", pwd):
                        if mixed:
                            salt = False
                        else:
                            # mixed not allowed so we convert plain passwords
                            # TODO: in further versions we should not convert
                            # the passwords anymore but we should skip the user
                            pwd = generate_password_hash(pwd)
                            self.conf.options[self.section][opt] = pwd
                            changed = True
                self._users[opt] = {"pwd": pwd, "salted": salt}
                self.logger.info(
                    "Loading user: {} ({})".format(opt, "hashed" if salt else "plain")
                )

            if changed:
                self.conf.options.write()
            # if not salted:
            #     self.conf.options.comments[self.section].append(
            #         '# Please DO NOT touch the following line'
            #     )
            #     self.conf.options.comments[self.section].append('# @salted@')
            #     self.conf.options.write()
            self.conf_id = self.conf.id
        return True

    def fetch(self, uid=None):
        """:func:`burpui.misc.auth.basic.BasicLoader.fetch` searches for a user
        in the configuration.

        :param uid: User to search for
        :type uid: str

        :returns: The given UID if the user exists or None
        """
        self.load_users()
        if uid in self.users:
            return uid

        return None

    def check(self, uid=None, passwd=None):
        """:func:`burpui.misc.auth.basic.BasicLoader.check` verifies if the
        given password matches the given user settings.

        :param uid: User to authenticate
        :type uid: str

        :param passwd: Password
        :type passwd: str

        :returns: True if there is a match, otherwise False
        """
        self.load_users()
        if uid in self.users:
            if self.users[uid]["salted"]:
                return check_password_hash(self.users[uid]["pwd"], passwd)
            else:
                return self.users[uid]["pwd"] == passwd
        return False

    @property
    def users(self):
        self.load_users()
        return self._users

    def _setup_users(self):
        """Setup user management"""
        if not self.conf.lookup_section(self.section):
            self.conf._refresh()

    def add_user(self, user, passwd):
        """Add a user"""
        self._setup_users()
        if user in self.users:
            message = "user '{}' already exists".format(user)
            self.logger.warning(message)
            return False, message, NOTIF_WARN
        pwd = generate_password_hash(passwd)
        self.conf.options[self.section][user] = pwd
        self.conf.options.write()
        self.load_users(True)
        message = "user '{}' successfully added".format(user)
        return True, message, NOTIF_OK

    def del_user(self, user):
        """Delete a user"""
        self._setup_users()
        self.load_users(True)
        if user not in self.users:
            message = "user '{}' does not exist".format(user)
            self.logger.error(message)
            return False, message, NOTIF_ERROR
        if user == "admin" and len(self.users.keys()) == 1:
            message = "trying to delete the admin account!"
            self.logger.warning(message)
            return False, message, NOTIF_WARN
        del self.conf.options[self.section][user]
        del self.users[user]
        self.conf.options.write()
        self.load_users(True)
        message = "user '{}' successfully removed".format(user)
        return True, message, NOTIF_OK

    def change_password(self, user, passwd, old_passwd=None):
        """Change a user password"""
        self._setup_users()
        old_users = self.users
        self.load_users(True)
        current = self.users.get(user, old_users.get(user, None))

        if not current:
            message = "user '{}' does not exist".format(user)
            self.logger.error(message)
            return False, message, NOTIF_ERROR

        if current["salted"]:
            comp = check_password_hash
        else:

            def comp(x, y):
                return x == y

        curr = current["pwd"]
        if old_passwd and not comp(curr, old_passwd):
            message = "unable to authenticate user '{}'".format(user)
            self.logger.error(message)
            return False, message, NOTIF_ERROR

        if comp(curr, passwd):
            message = "password is the same"
            self.logger.warning(message)
            return False, message, NOTIF_WARN

        pwd = generate_password_hash(passwd)
        self.conf.options[self.section][user] = pwd
        self.conf.options.write()
        self.load_users(True)
        message = "user '{}' successfully updated".format(user)
        return True, message, NOTIF_OK


class UserHandler(BUIhandler):
    __doc__ = __("Uses the Burp-UI configuration file to load its rules.")
    priority = 100

    """See :class:`burpui.misc.auth.interface.BUIhandler`"""

    def __init__(self, app=None):
        """See :func:`burpui.misc.auth.interface.BUIhandler.__init__`"""
        self.basic = BasicLoader(app, self)
        self.change = False
        self.users = {}

    def user(self, name=None):
        """See :func:`burpui.misc.auth.interface.BUIhandler.user`"""
        if name not in self.users:
            self.change = self.basic.load_users()
            self.users[name] = BasicUser(self.basic, name)
        ret = self.users[name]
        if not ret.active:
            return AnonymousUserMixin()
        return ret

    @property
    def changed(self):
        ret = self.change or self.basic.load_users()
        self.change = False
        return ret

    @property
    def loader(self):
        return self.basic


class BasicUser(BUIuser):
    """See :class:`burpui.misc.auth.interface.BUIuser`"""

    def __init__(self, basic=None, name=None):
        self.active = False
        self.authenticated = False
        self.basic = basic
        self.name = name
        self.id = None
        self.backend = self.basic.name

        res = self.basic.fetch(self.name)

        if res:
            self.id = res
            self.active = True

    def login(self, passwd=None):
        """See :func:`burpui.misc.auth.interface.BUIuser.login`"""
        self.authenticated = self.basic.check(self.name, passwd)
        return self.authenticated

    def get_id(self):
        return self.id
