import datetime
from typing import List, Union

import pytz
from django.core.exceptions import ValidationError
from django.db.models import Model

from nwon_django_toolbox.typings import AllowedUpdate


def validate_model_update(
    old_model_instance: Model,
    updated_model_instance: Model,
    allowed_updates: Union[List[AllowedUpdate], None],
):
    """
    Checks an update model instance against an allowed set of fields and their values
    """

    if updated_model_instance._state.adding is True:
        return

    if not allowed_updates:
        return

    fields = updated_model_instance._meta.get_fields()

    allowed_field_names = [update.field_name for update in allowed_updates]

    if old_model_instance.__class__ != updated_model_instance.__class__:
        raise ValidationError(
            f"You tried to validate update for two instances of a different class: "
            f"{old_model_instance.__class__}  and "
            f"{updated_model_instance.__class__}"
        )

    if old_model_instance.pk != updated_model_instance.pk:
        raise ValidationError(
            f"You tried to validate update for two instances of "
            f"{old_model_instance.__class__} with a different pk value: "
            f"{old_model_instance.pk} and {updated_model_instance.pk}"
        )

    for field in fields:
        field_name = field.name
        old_value = getattr(old_model_instance, field_name)
        new_value = getattr(updated_model_instance, field_name)

        if isinstance(old_value, datetime.datetime) and old_value.tzinfo is None:
            old_value = pytz.utc.localize(old_value)

        if isinstance(new_value, datetime.datetime) and new_value.tzinfo is None:
            new_value = pytz.utc.localize(new_value)

        if old_value == new_value:
            continue

        if not field_name in allowed_field_names:
            raise ValidationError(
                {
                    field_name: f"You are not allowed to update {field_name} "
                    f"with value {new_value} from {old_value} for "
                    f"{updated_model_instance.__class__} "
                    f"with pk {updated_model_instance.pk}"
                }
            )

        allowed_updates_for_field_name = _filter_allowed_updates(
            allowed_updates, field_name
        )

        # There is some configuration which limits possible field values
        new_value_has_constraint = any(
            x.allowed_values is not None for x in allowed_updates_for_field_name
        )

        # The new value is explicitly allowed in some field configuration
        new_value_matches_constraint = any(
            new_value in x.allowed_values
            for x in allowed_updates_for_field_name
            if x.allowed_values
        )

        if new_value and new_value_has_constraint and not new_value_matches_constraint:
            raise ValidationError(
                {
                    field_name: f"{new_value} can't be set as value for "
                    f"{field_name} in {updated_model_instance} with pk "
                    f"{updated_model_instance.pk}"
                }
            )


def _filter_allowed_updates(
    allowed_updates: List[AllowedUpdate], field_name: str
) -> List[AllowedUpdate]:
    return list(
        filter(
            lambda x: field_name == x.field_name and x.allowed_values,
            allowed_updates,
        )
    )
