import uuid

from django.conf import settings
from django.contrib.auth.models import Permission
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db import models, router, transaction

from apps.organization.assignment_locks import lock_user
from apps.organization.assignments import UserOrganizationAssignment
from apps.organization.model_base import TimeStampedModel
from .locks import share_rows

CODE_PATTERN = r"^[A-Z0-9][A-Z0-9_-]*$"


class ActiveQuerySet(models.QuerySet):
    def active(self):
        return self.filter(is_active=True)


class Role(TimeStampedModel):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    code = models.CharField(max_length=64, validators=[RegexValidator(CODE_PATTERN, "Use uppercase letters, digits, hyphens or underscores.")])
    name = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    is_active = models.BooleanField(default=True)
    permissions = models.ManyToManyField(Permission, blank=True, related_name="business_roles")
    objects = ActiveQuerySet.as_manager()

    class Meta:
        ordering = ["code"]
        constraints = [
            models.CheckConstraint(condition=models.Q(code__regex=CODE_PATTERN), name="access_role_code_format"),
            models.UniqueConstraint(fields=["code"], name="access_role_code_uniq"),
        ]

    def clean_fields(self, exclude=None):
        if isinstance(self.code, str):
            self.code = self.code.strip().upper()
        super().clean_fields(exclude=exclude)

    def clean(self):
        super().clean()
        if not self.is_active and self.pk and self.user_assignments.filter(is_active=True).exists():
            raise ValidationError({"is_active": "Active role assignments exist. Use the role deactivation service/action."})

    def save(self, *, force_insert=False, force_update=False, using=None, update_fields=None):
        using = using or router.db_for_write(type(self), instance=self)
        if update_fields is not None:
            update_fields = set(update_fields)
            if not update_fields:
                return
            update_fields.add("updated_at")
        with transaction.atomic(using=using):
            previous = type(self).objects.using(using).select_for_update().filter(pk=self.pk).first()
            self.full_clean()
            if previous is not None and update_fields is not None:
                for field in self._meta.concrete_fields:
                    if field.name in update_fields or field.attname in update_fields:
                        setattr(previous, field.attname, getattr(self, field.attname))
                previous.full_clean()
            return super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)

    def __str__(self):
        return f"{self.code} - {self.name}"


class RoleAssignmentQuerySet(ActiveQuerySet):
    def for_user(self, user):
        return self.filter(user=user)


class UserRoleAssignment(TimeStampedModel):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="business_role_assignments")
    role = models.ForeignKey(Role, on_delete=models.PROTECT, related_name="user_assignments")
    organization_assignment = models.ForeignKey(UserOrganizationAssignment, on_delete=models.PROTECT, related_name="role_assignments")
    is_active = models.BooleanField(default=True)
    objects = RoleAssignmentQuerySet.as_manager()

    class Meta:
        ordering = ["-created_at", "id"]
        constraints = [models.UniqueConstraint(
            fields=["user", "role", "organization_assignment"], condition=models.Q(is_active=True),
            name="access_active_role_assignment_uniq",
        )]

    def clean(self):
        super().clean()
        errors = {}
        original_user = type(self).objects.filter(pk=self.pk).values_list("user_id", flat=True).first()
        if original_user is not None and original_user != self.user_id:
            errors["user"] = "A role assignment cannot be transferred to another user."
        for name in ["user", "role", "organization_assignment"]:
            pk = getattr(self, f"{name}_id")
            parent = self._meta.get_field(name).remote_field.model.objects.filter(pk=pk).first() if pk else None
            if parent is None:
                continue  # full_clean field validation reports missing parents.
            if name == "organization_assignment" and parent.user_id != self.user_id:
                errors[name] = "Organization assignment must belong to the selected user."
            elif self.is_active and not parent.is_active:
                errors[name] = "An active role assignment requires an active dependency."
        if errors:
            raise ValidationError(errors)

    def save(self, *, force_insert=False, force_update=False, using=None, update_fields=None):
        using = using or router.db_for_write(type(self), instance=self)
        if update_fields is not None:
            update_fields = set(update_fields)
            if not update_fields:
                return
            update_fields.add("updated_at")
        with transaction.atomic(using=using):
            previous = type(self).objects.using(using).filter(pk=self.pk).first()
            user_id = previous.user_id if previous else self.user_id
            if user_id is None:
                self.full_clean()
            lock_user(user_id, using)
            previous = type(self).objects.using(using).filter(pk=self.pk).first()
            share_rows(UserOrganizationAssignment, [self.organization_assignment_id,
                       previous.organization_assignment_id if previous else None], using)
            share_rows(Role, [self.role_id, previous.role_id if previous else None], using)
            self.full_clean()
            if previous is not None and update_fields is not None:
                for field in self._meta.concrete_fields:
                    if field.name in update_fields or field.attname in update_fields:
                        setattr(previous, field.attname, getattr(self, field.attname))
                previous.full_clean()
            return super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)

    def __str__(self):
        return f"Role assignment {self.pk} (user {self.user_id})"
