"""Global product definitions; no physical-device or organizational scope fields."""
import uuid

from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db import connections, models, router, transaction

from apps.organization.model_base import TimeStampedModel

CODE_PATTERN = r"^[A-Z0-9][A-Z0-9_-]*$"


def _share(model, pk, using):
    if pk is None:
        return
    connection = connections[using]
    table = connection.ops.quote_name(model._meta.db_table)
    with connection.cursor() as cursor:
        cursor.execute(f"SELECT id FROM {table} WHERE id = %s FOR SHARE", [pk])
        cursor.fetchall()


class CatalogQuerySet(models.QuerySet):
    def active(self):
        return self.filter(is_active=True)


class CatalogRecord(TimeStampedModel):
    """App-local fields and validated writes, without implicit cascades."""
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    code = models.CharField(max_length=64, validators=[RegexValidator(
        CODE_PATTERN, "Use letters, digits, hyphens or underscores; start with a letter or digit.")])
    name = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    is_active = models.BooleanField(default=True)
    objects = CatalogQuerySet.as_manager()

    class Meta:
        abstract = True
        ordering = ["code"]
        constraints = [models.CheckConstraint(condition=models.Q(code__regex=CODE_PATTERN),
            name="%(app_label)s_%(class)s_code_format")]

    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:
            if isinstance(self, (Brand, ProductCategory)):
                children = self.product_models.filter(is_active=True).exists()
                variants = ProductVariant.objects.filter(
                    **{f"product_model__{'brand' if isinstance(self, Brand) else 'category'}_id": self.pk},
                    is_active=True).exists()
                children = children or variants
            elif isinstance(self, ProductModel):
                children = self.variants.filter(is_active=True).exists()
            else:
                children = False
            if children:
                raise ValidationError({"is_active": "Active descendants exist. Use the 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):
            if isinstance(self, ProductModel):
                _share(Brand, self.brand_id, using)
                _share(ProductCategory, self.category_id, using)
            elif isinstance(self, ProductVariant):
                # Every ancestor cascade exclusively locks affected models first.
                _share(ProductModel, self.product_model_id, 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 Brand(CatalogRecord):
    class Meta(CatalogRecord.Meta):
        abstract = False
        constraints = CatalogRecord.Meta.constraints + [
            models.UniqueConstraint(fields=["code"], name="catalog_brand_code_uniq")]


class ProductCategory(CatalogRecord):
    class Meta(CatalogRecord.Meta):
        abstract = False
        verbose_name_plural = "product categories"
        constraints = CatalogRecord.Meta.constraints + [
            models.UniqueConstraint(fields=["code"], name="catalog_category_code_uniq")]


class ProductModel(CatalogRecord):
    brand = models.ForeignKey(Brand, on_delete=models.PROTECT, related_name="product_models")
    category = models.ForeignKey(ProductCategory, on_delete=models.PROTECT, related_name="product_models")

    class Meta(CatalogRecord.Meta):
        abstract = False
        constraints = CatalogRecord.Meta.constraints + [
            models.UniqueConstraint(fields=["brand", "code"], name="catalog_model_brand_code_uniq")]

    def clean(self):
        super().clean()
        original = type(self).objects.filter(pk=self.pk).values_list("brand_id", flat=True).first()
        if original is not None and original != self.brand_id:
            raise ValidationError({"brand": "Brand ownership cannot change after creation."})
        errors = {}
        for name, model in [("brand", Brand), ("category", ProductCategory)]:
            if self.is_active and model.objects.filter(pk=getattr(self, f"{name}_id"), is_active=False).exists():
                errors[name] = "An active product model requires active Brand and Category."
        if errors:
            raise ValidationError(errors)


class ProductVariant(CatalogRecord):
    product_model = models.ForeignKey(ProductModel, on_delete=models.PROTECT, related_name="variants")

    class Meta(CatalogRecord.Meta):
        abstract = False
        constraints = CatalogRecord.Meta.constraints + [
            models.UniqueConstraint(fields=["product_model", "code"], name="catalog_variant_model_code_uniq")]

    def clean(self):
        super().clean()
        original = type(self).objects.filter(pk=self.pk).values_list("product_model_id", flat=True).first()
        if original is not None and original != self.product_model_id:
            raise ValidationError({"product_model": "ProductModel ownership cannot change after creation."})
        parent = ProductModel.objects.filter(pk=self.product_model_id).values(
            "is_active", "brand__is_active", "category__is_active").first()
        if self.is_active and parent is not None and not all(parent.values()):
            raise ValidationError({"product_model": "An active variant requires an active Model, Brand and Category."})


class DeviceIdentificationPolicy(TimeStampedModel):
    """Requirements only. Physical identifier values belong to a future domain."""
    class Requirement(models.TextChoices):
        NOT_APPLICABLE = "NOT_APPLICABLE", "Not applicable"
        OPTIONAL = "OPTIONAL", "Optional"
        REQUIRED = "REQUIRED", "Required"

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    product_model = models.OneToOneField(
        ProductModel, on_delete=models.PROTECT, related_name="identification_policy")
    # No defaults: creating a policy requires a deliberate choice for each type.
    imei1_requirement = models.CharField(max_length=14, choices=Requirement.choices)
    imei2_requirement = models.CharField(max_length=14, choices=Requirement.choices)
    serial_requirement = models.CharField(max_length=14, choices=Requirement.choices)

    class Meta:
        ordering = ["product_model_id"]
        verbose_name_plural = "device identification policies"
        constraints = [
            models.CheckConstraint(condition=models.Q(imei1_requirement__in=["NOT_APPLICABLE", "OPTIONAL", "REQUIRED"]),
                name="catalog_policy_imei1_valid"),
            models.CheckConstraint(condition=models.Q(imei2_requirement__in=["NOT_APPLICABLE", "OPTIONAL", "REQUIRED"]),
                name="catalog_policy_imei2_valid"),
            models.CheckConstraint(condition=models.Q(serial_requirement__in=["NOT_APPLICABLE", "OPTIONAL", "REQUIRED"]),
                name="catalog_policy_serial_valid"),
            models.CheckConstraint(condition=models.Q(imei2_requirement="NOT_APPLICABLE") | ~models.Q(imei1_requirement="NOT_APPLICABLE"),
                name="catalog_policy_imei2_needs_imei1"),
        ]

    def clean(self):
        super().clean()
        errors = {}
        original = type(self).objects.filter(pk=self.pk).values_list("product_model_id", flat=True).first()
        if original is not None and original != self.product_model_id:
            errors["product_model"] = "Policy ownership cannot change after creation."
        if self.imei2_requirement in (self.Requirement.OPTIONAL, self.Requirement.REQUIRED) and self.imei1_requirement == self.Requirement.NOT_APPLICABLE:
            errors["imei2_requirement"] = "IMEI2 requires IMEI1 to be Optional or Required."
        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):
            original = type(self).objects.using(using).filter(pk=self.pk).values_list("product_model_id", flat=True).first()
            # Stable ownership lets all supported policy writes serialize on this
            # parent, including when the first policy row does not exist yet.
            ProductModel.objects.using(using).select_for_update().filter(pk=original or self.product_model_id).first()
            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"Identification policy (model {self.product_model_id})"
