"""Core models."""

import re
from email.header import Header

from django.conf import settings
from django.db import models
from django.urls import reverse
from django.utils.encoding import force_str, smart_bytes, smart_text
from django.utils.functional import cached_property
from django.utils.translation import ugettext as _, ugettext_lazy

from django.contrib.auth.models import AbstractUser, Group
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType

import jsonfield
from phonenumber_field.modelfields import PhoneNumberField
from reversion import revisions as reversion

from modoboa.core.password_hashers import get_password_hasher
from modoboa.lib.exceptions import (
    BadRequest, Conflict, InternalError, PermDeniedException
)
from modoboa.parameters import tools as param_tools
from . import constants, signals

try:
    from modoboa.lib.ldap_utils import LDAPAuthBackend
    ldap_available = True
except ImportError:
    ldap_available = False


class User(AbstractUser):

    """Custom User model.

    It overloads the way passwords are stored into the database. The
    main reason to change this mechanism is to ensure the
    compatibility with the way Dovecot stores passwords.

    It also adds new attributes and methods.
    """

    username = models.CharField(max_length=254, unique=True)
    email = models.EmailField(max_length=254, blank=True, db_index=True)
    is_staff = models.BooleanField(default=False, db_index=True)
    is_active = models.BooleanField(default=True, db_index=True)
    is_local = models.BooleanField(default=True, db_index=True)
    master_user = models.BooleanField(
        ugettext_lazy("Allow mailboxes access"), default=False,
        help_text=ugettext_lazy(
            "Allow this administrator to access user mailboxes"
        )
    )
    password = models.CharField(ugettext_lazy("password"), max_length=256)

    language = models.CharField(
        ugettext_lazy("language"),
        max_length=10, default="en", choices=constants.LANGUAGES,
        help_text=ugettext_lazy(
            "Prefered language to display pages."
        )
    )
    phone_number = PhoneNumberField(
        ugettext_lazy("Phone number"), blank=True, null=True)
    secondary_email = models.EmailField(
        ugettext_lazy("Secondary email"), max_length=254,
        blank=True, null=True,
        help_text=ugettext_lazy(
            "An alternative e-mail address, can be used for recovery needs.")
    )

    tfa_enabled = models.BooleanField(default=False)

    _parameters = jsonfield.JSONField(default={})

    class Meta(object):
        ordering = ["username"]
        index_together = [
            ["email", "is_active"]
        ]

    password_expr = re.compile(r'\{([\w\-]+)\}(.+)')

    def __init__(self, *args, **kwargs):
        """Load parameter manager."""
        super(User, self).__init__(*args, **kwargs)
        self.parameters = param_tools.Manager("user", self._parameters)

    def _crypt_password(self, raw_value):

        """Crypt the local password using the appropriate scheme.

        In case we don't find the scheme (for example when the
        management framework is used), we load the parameters and try
        one more time.

        """
        scheme = param_tools.get_global_parameter(
            "password_scheme", raise_exception=False)
        if scheme is None:
            from modoboa.core.apps import load_core_settings
         
   load_core_settings()
            scheme = param_tools.get_global_parameter(
                "password_scheme", raise_exception=False)
        raw_value = smart_bytes(raw_value)
        return get_password_hasher(scheme.upper())().encrypt(raw_value)

    def set_password(self, raw_value, curvalue=None):
        """Password update

        Update the current mailbox's password with the given clear
        value. This value is encrypted according to the defined method
        before it is saved.

        :param raw_value: the new password's value
        :param curvalue: the current password (for LDAP authentication)
        """
        ldap_sync_enable = param_tools.get_global_parameter("ldap_enable_sync")
        if self.is_local or ldap_sync_enable:
            self.password = self._crypt_password(raw_value)
        else:
            if not ldap_available:
                raise InternalError(
                    _("Failed to update password: LDAP module not installed")
                )
            LDAPAuthBackend().update_user_password(
                self.username, curvalue, raw_value
            )
        signals.account_password_updated.send(
            sender=self.__class__,
            account=self, password=raw_value, created=self.pk is None)

    def check_password(self, raw_value):
        """Compare raw_value to current password."""
        match = self.password_expr.match(self.password)
        if match is None:
            return False
        raw_value = force_str(raw_value)
        scheme = match.group(1)
        val2 = match.group(2)
        hasher = get_password_hasher(scheme)
        return hasher().verify(raw_value, val2)

    def __str__(self):
        return smart_text(self.get_username())

    def get_absolute_url(self):
        """Return detail url for this user."""
        return reverse("admin:account_detail", args=[self.pk])

    @property
    def type(self):
        return "account"

    @property
    def tags(self):
        return [{"name": "account", "label": _("account"), "type": "idt"},
                {"name": self.role, "label": self.role,
                 "type": "grp", "color": "info"}]

    @property
    def fullname(self):
        result = self.username
        if self.first_name != "":
            result = self.first_name
        if self.last_name != "":
            if result != "":
                result += " "
            result += self.last_name
        return result

    @property
    def identity(self):
        return self.username

    @property
    def name_or_rcpt(self):
        if self.first_name != "":
            return "%s %s" % (self.first_name, self.last_name)
        return "----"

    @property
    def enabled(self):
        return self.is_active

    @property
    def encoded_address(self):
        if self.first_name != "" or self.last_name != "":
            return '"{}" <{}>'.format(
                Header(self.fullname, "utf8").encode(), self.email)
        return self.email

    def is_owner(self, obj):
        """Tell is the user is the unique owner of this object

        :param obj: an object inheriting from ``models.Model``
        :return: a boolean
        """
        ct = ContentType.objects.get_for_model(obj)
        try:
            ooentry = self.objectaccess_set.get(
                content_type=ct, object_id=obj.id)
        except ObjectAccess.DoesNotExist:
            return False
        return ooentry.is_owner

    def can_access(self, obj):
        """Check if the user can access a specific object

        This function is recursive: if the given user hasn't got
        direct access to this object and if he has got access to other
        ``User`` objects, we check if one of those users owns the
        object.

        :param obj: a admin object
        :return: a boolean
        """
        if self.is_superuser:
            return True

        ct = ContentType.objects.get_for_model(obj)
        try:
            ooentry = self.objectaccess_set.get(
                content_type=ct, object_id=obj.id)
        except ObjectAccess.DoesNotExist:
            pass
        else:
            return True
        if ct.model == "user":
            return False

        ct = ContentType.objects.get_for_model(self)
        qs = self.objectaccess_set.filter(content_type=ct)
        for ooentry in qs.all():
            if ooentry.content_object.is_owner(obj):
                return True
        return False

    @property
    def role(self):
        """Return user role."""
        if not hasattr(self, "_role"):
            if self.is_superuser:
                self._role = "SuperAdmins"
            else:
                try:
    