from dataclasses import dataclass, field
from enum import Enum
from typing import Any

from django import forms
from django.contrib.postgres.forms import SimpleArrayField
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.utils.translation import gettext_lazy as _
from jsonschema.exceptions import SchemaError
from jsonschema.validators import validator_for

from utilities.string import title
from utilities.validators import MultipleOfValidator

__all__ = (
    'JSONSchemaProperty',
    'PropertyTypeEnum',
    'StringFormatEnum',
    'validate_schema',
)


class PropertyTypeEnum(Enum):
    STRING = 'string'
    INTEGER = 'integer'
    NUMBER = 'number'
    BOOLEAN = 'boolean'
    ARRAY = 'array'
    OBJECT = 'object'


class StringFormatEnum(Enum):
    EMAIL = 'email'
    URI = 'uri'
    IRI = 'iri'
    UUID = 'uuid'
    DATE = 'date'
    TIME = 'time'
    DATETIME = 'datetime'


FORM_FIELDS = {
    PropertyTypeEnum.STRING.value: forms.CharField,
    PropertyTypeEnum.INTEGER.value: forms.IntegerField,
    PropertyTypeEnum.NUMBER.value: forms.FloatField,
    PropertyTypeEnum.BOOLEAN.value: forms.BooleanField,
    PropertyTypeEnum.ARRAY.value: SimpleArrayField,
    PropertyTypeEnum.OBJECT.value: forms.JSONField,
}

STRING_FORM_FIELDS = {
    StringFormatEnum.EMAIL.value: forms.EmailField,
    StringFormatEnum.URI.value: forms.URLField,
    StringFormatEnum.IRI.value: forms.URLField,
    StringFormatEnum.UUID.value: forms.UUIDField,
    StringFormatEnum.DATE.value: forms.DateField,
    StringFormatEnum.TIME.value: forms.TimeField,
    StringFormatEnum.DATETIME.value: forms.DateTimeField,
}


@dataclass
class JSONSchemaProperty:
    type: PropertyTypeEnum = PropertyTypeEnum.STRING.value
    title: str | None = None
    description: str | None = None
    default: Any = None
    enum: list | None = None

    # Strings
    minLength: int | None = None
    maxLength: int | None = None
    pattern: str | None = None  # Regex
    format: StringFormatEnum | None = None

    # Numbers
    minimum: int | float | None = None
    maximum: int | float | None = None
    multipleOf: int | float | None = None

    # Arrays
    items: dict | None = field(default_factory=dict)

    def to_form_field(self, name, required=False):
        """
        Instantiate and return a Django form field suitable for editing the property's value.
        """
        field_kwargs = {
            'label': self.title or title(name),
            'help_text': self.description,
            'required': required,
            'initial': self.default,
        }

        # Choices
        if self.enum:
            choices = [(v, v) for v in self.enum]
            if not required:
                choices = [(None, ''), *choices]
            field_kwargs['choices'] = choices

        # Arrays
        if self.type == PropertyTypeEnum.ARRAY.value:
            items_type = self.items.get('type', PropertyTypeEnum.STRING.value)
            field_kwargs['base_field'] = FORM_FIELDS[items_type]()

        # String validation
        if self.type == PropertyTypeEnum.STRING.value:
            if self.minLength is not None:
                field_kwargs['min_length'] = self.minLength
            if self.maxLength is not None:
                field_kwargs['max_length'] = self.maxLength
            if self.pattern is not None:
                field_kwargs['validators'] = [
                    RegexValidator(regex=self.pattern)
                ]

        # Integer/number validation
        elif self.type in (PropertyTypeEnum.INTEGER.value, PropertyTypeEnum.NUMBER.value):
            field_kwargs['widget'] = forms.NumberInput(attrs={'step': 'any'})
            if self.minimum:
                field_kwargs['min_value'] = self.minimum
            if self.maximum:
                field_kwargs['max_value'] = self.maximum
            if self.multipleOf:
                field_kwargs['validators'] = [
                    MultipleOfValidator(multiple=self.multipleOf)
                ]

        return self.field_class(**field_kwargs)

    @property
    def field_class(self):
        """
        Resolve the property's type (and string format, if specified) to the appropriate field class.
        """
        if self.enum:
            if self.type == PropertyTypeEnum.ARRAY.value:
                return forms.MultipleChoiceField
            return forms.ChoiceField
        if self.type == PropertyTypeEnum.STRING.value and self.format is not None:
            try:
                return STRING_FORM_FIELDS[self.format]
            except KeyError:
                raise ValueError(f"Unsupported string format type: {self.format}")
        try:
            return FORM_FIELDS[self.type]
        except KeyError:
            raise ValueError(f"Unknown property type: {self.type}")


def validate_schema(schema):
    """
    Check that a minimum JSON schema definition is defined.
    """
    # Provide some basic sanity checking (not provided by jsonschema)
    if not schema or type(schema) is not dict:
        raise ValidationError(_("Invalid JSON schema definition"))
    if not schema.get('properties'):
        raise ValidationError(_("JSON schema must define properties"))
    try:
        ValidatorClass = validator_for(schema)
        ValidatorClass.check_schema(schema)
    except SchemaError as e:
        raise ValidationError(_("Invalid JSON schema definition: {error}").format(error=e))
