# type: ignore
from typing import Any

from django import VERSION
from django.core.exceptions import FieldError
from django.db import connections
from django.db.models import CheckConstraint, Q, UniqueConstraint
from django.db.models.expressions import Exists, F, OrderBy
from django.db.models.lookups import Exact, IsNull
from django.db.utils import DEFAULT_DB_ALIAS
from ..exceptions import UniqueConstraintValidationError, EXISTING_RECORD_CID_KEY
from ..utils import lookup_field


class RequiredStringConstraint(CheckConstraint):
    """
    Prevent empty strings from being saved to the database.
    One way this can happen if a model is instantiated, and then
    "save" is called without calling the "full_clean" method.
    """

    def __init__(self, *args: Any, **kwargs: Any) -> None: 
        field = kwargs.pop('field', '')

        # TODO simplify "condition_field" to "condition" after dropping ClearOS 6
        if 'check' in kwargs or VERSION < (5, 1):
            condition_field = 'check'
        else:
            condition_field = 'condition'

        if not field and condition_field not in kwargs:
            raise ValueError('"field" value must be provided')
        condition = kwargs.get(condition_field) or ~Q((field, ''))

        # append field name to the end so we can parse field name
        # from index name even if field name contains underscores
        name = kwargs.get('name') or f"%(app_label)s_%(class)s_required_string_{field}"

        kwargs |= {
            'name': name,
            condition_field: condition,
        }
        super().__init__(*args, **kwargs)


class LCUniqueConstraint(UniqueConstraint):

    def __init__(self, *args: Any, **kwargs: Any) -> None: 
        """
        Override to specify the field to which
        the ValidationError should be attached.
        """
        self.error_field = kwargs.pop('error_field', '')
        super().__init__(*args, **kwargs)

    def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS):
        """
        Override default method to return the Canonical ID of
        the existing unique row that satisfies the constraint.
        """
        if not lookup_field(model, 'cid').name:
            return super().validate(model=model, instance=instance, exclude=exclude, using=using)

        if VERSION >= (5, 1):
            return self._django_5_1_validate(model, instance, exclude, using)
        else:
            return self._django_5_0_validate(model, instance, exclude, using)

    def _django_5_1_validate(self, model, instance, exclude, using):
        # TODO consolidate into validate after we no longer support ClearOS 6
        # Now that we now the model as a `cid` field, raise UniqueConstraintValidationError
        queryset = model._default_manager.using(using)
        if self.fields:
            lookup_kwargs = {}
            for field_name in self.fields:
                if exclude and field_name in exclude:
                    return
                field = model._meta.get_field(field_name)
                lookup_value = getattr(instance, field.attname)
                if (
                    self.nulls_distinct is not False
                    and lookup_value is None
                    or (
                        lookup_value == ""
                        and connections[
                            using
                        ].features.interprets_empty_strings_as_nulls
                    )
                ):
                    # A composite constraint containing NULL value cannot cause
                    # a violation since NULL != NULL in SQL.
                    return
                lookup_kwargs[field.name] = lookup_value
            queryset = queryset.filter(**lookup_kwargs)
        else:
            # Ignore constraints with excluded fields.
            if exclude:
                for expression in self.expressions:
                    if hasattr(expression, "flatten"):
                        for expr in expression.flatten():
                            if isinstance(expr, F) and expr.name in exclude:
                                return
                    elif isinstance(expression, F) and expression.name in exclude:
                        return
            replacements = {
                F(field): value
                for field, value in instance._get_field_expression_map(
                    meta=model._meta, exclude=exclude
                ).items()
            }
            filters = []
            for expr in self.expressions:
                if hasattr(expr, "get_expression_for_validation"):
                    expr = expr.get_expression_for_validation()
                rhs = expr.replace_expressions(replacements)
                condition = Exact(expr, rhs)
                if self.nulls_distinct is False:
                    condition = Q(condition) | Q(IsNull(expr, True), IsNull(rhs, True))
                filters.append(condition)
            queryset = queryset.filter(*filters)
        model_class_pk = instance._get_pk_val(model._meta)
        if not instance._state.adding and model_class_pk is not None:
            queryset = queryset.exclude(pk=model_class_pk)
        if not self.condition:
            if existing_row_cid := queryset.values_list('cid', flat=True).first():
                if self.fields:
                    # When fields are defined, use the unique_error_message() for
                    # backward compatibility.
                    for model, constraints in instance.get_constraints():
                        for constraint in constraints:
                            if constraint is self:
                                raise UniqueConstraintValidationError(
                                    **{
                                        'message': instance.unique_error_message(model, self.fields),
                                        EXISTING_RECORD_CID_KEY: existing_row_cid,
                                    },
                                )
                raise UniqueConstraintValidationError(
                    **{
                        'message': self.get_violation_error_message(),
                        'code': self.violation_error_code,
                        EXISTING_RECORD_CID_KEY: existing_row_cid,
                    },
                )
        else:
            against = instance._get_field_expression_map(
                meta=model._meta, exclude=exclude
            )
            try:
                if (self.condition & Exists(queryset.filter(self.condition))).check(
                    against, using=using
                ):
                    existing_row_cid = queryset.filter(
                        self.condition,
                    ).values_list(
                        'cid', flat=True
                    ).first()
                    raise UniqueConstraintValidationError(
                        **{
                            'message': self.get_violation_error_message(),
                            'code': self.violation_error_code,
                            EXISTING_RECORD_CID_KEY: existing_row_cid,
                        },
                    )
            except FieldError:
                pass

    def _django_5_0_validate(self, model, instance, exclude, using):
        # TODO - Delete this after we no longer support ClearOS 6
        queryset = model._default_manager.using(using)
        if self.fields:
            lookup_kwargs = {}
            for field_name in self.fields:
                if exclude and field_name in exclude:
                    return
                field = model._meta.get_field(field_name)
                lookup_value = getattr(instance, field.attname)
                if (
                    self.nulls_distinct is not False
                    and lookup_value is None
                    or (
                        lookup_value == ""
                        and connections[
                            using
                        ].features.interprets_empty_strings_as_nulls
                    )
                ):
                    # A composite constraint containing NULL value cannot cause
                    # a violation since NULL != NULL in SQL.
                    return
                lookup_kwargs[field.name] = lookup_value
            queryset = queryset.filter(**lookup_kwargs)
        else:
            # Ignore constraints with excluded fields.
            if exclude:
                for expression in self.expressions:
                    if hasattr(expression, "flatten"):
                        for expr in expression.flatten():
                            if isinstance(expr, F) and expr.name in exclude:
                                return
                    elif isinstance(expression, F) and expression.name in exclude:
                        return
            replacements = {
                F(field): value
                for field, value in instance._get_field_value_map(
                    meta=model._meta, exclude=exclude
                ).items()
            }
            expressions = []
            for expr in self.expressions:
                # Ignore ordering.
                if isinstance(expr, OrderBy):
                    expr = expr.expression
                expressions.append(Exact(expr, expr.replace_expressions(replacements)))
            queryset = queryset.filter(*expressions)
        model_class_pk = instance._get_pk_val(model._meta)
        if not instance._state.adding and model_class_pk is not None:
            queryset = queryset.exclude(pk=model_class_pk)
        if not self.condition:
            if existing_row_cid := queryset.values_list('cid', flat=True).first():
                if self.expressions:
                    raise UniqueConstraintValidationError(
                        **{
                            'message': self.get_violation_error_message(),
                            'code': self.violation_error_code,
                            EXISTING_RECORD_CID_KEY: existing_row_cid,
                        },
                    )
                # When fields are defined, use the unique_error_message() for
                # backward compatibility.
                for model, constraints in instance.get_constraints():
                    for constraint in constraints:
                        if constraint is self:
                            raise UniqueConstraintValidationError(
                                **{
                                    'message': instance.unique_error_message(model, self.fields),
                                    EXISTING_RECORD_CID_KEY: existing_row_cid,
                                },
                            )
        else:
            against = instance._get_field_value_map(meta=model._meta, exclude=exclude)
            try:
                if (self.condition & Exists(queryset.filter(self.condition))).check(
                    against, using=using
                ):
                    existing_row_cid = queryset.filter(
                        self.condition,
                    ).values_list(
                        'cid', flat=True
                    ).first()
                    raise UniqueConstraintValidationError(
                        **{
                            'message': self.get_violation_error_message(),
                            'code': self.violation_error_code,
                            EXISTING_RECORD_CID_KEY: existing_row_cid,
                        },
                    )
            except FieldError:
                pass


__all__ = (
    'RequiredStringConstraint',
    'LCUniqueConstraint',
)
