###############################################################################
#
# (C) Copyright 2025 EVERYSK TECHNOLOGIES
#
# This is an unpublished work containing confidential and proprietary
# information of EVERYSK TECHNOLOGIES. Disclosure, use, or reproduction
# without authorization of EVERYSK TECHNOLOGIES is prohibited.
#
###############################################################################
import contextlib
import re
from collections.abc import Callable, Iterator
from sys import maxsize as max_int
from types import GenericAlias, UnionType
from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, Union, get_args, get_origin
from urllib.parse import urlparse

from everysk.core.datetime import Date, DateTime
from everysk.core.exceptions import ReadonlyError, RequiredError
from everysk.utils import bool_convert

FT = TypeVar('FT')
if TYPE_CHECKING:
    from everysk.core.mapping.base import BaseMapping


## Internal functions
def _do_nothing(*args, **kwargs) -> None:
    # pylint: disable=unused-argument
    raise ReadonlyError('This field value cannot be changed.')


def get_field_type(field_type: Any) -> type:
    """
    Get the correct field type from the given field type.
    We need to handle GenericAlias and UnionType to get the correct internal type.

    Args:
        field_type (FT): The field type to process.
    """
    if isinstance(field_type, GenericAlias):
        # We need to get the origin of the generic type
        field_type = get_origin(field_type)

    # TypeError: typing.Union cannot be used with isinstance()
    # The correct class of Unions is types.UnionType
    elif isinstance(field_type, UnionType):
        # We need to find if there are GenericAlias inside the Union
        types = []
        for _type in get_args(field_type):
            if isinstance(_type, GenericAlias):
                _type = get_origin(_type)
            # We discard NoneType from the Union because it's normally used as optional
            # This check is here because None is always the last type in the Union
            if _type is type(None):
                continue

            types.append(_type)

        # If there is only one type, we use it directly
        if len(types) == 1:
            field_type = types[0]
        else:
            field_type = Union[tuple(types)]  # noqa: UP007

    return field_type


# https://docs.astral.sh/ruff/rules/non-pep695-generic-class
# To Python 3.11 we still need to use Generic for classes
class BaseMappingField(Generic[FT]):  # noqa: UP046
    # https://docs.python.org/3.12/howto/descriptor.html
    choices: set[FT] | None = None
    default: FT = Undefined
    field_name: str | None = None
    field_type: FT | None = None
    readonly: bool = False
    required: bool = False

    ## Internal methods
    def __init__(
        self,
        default: FT | None = Undefined,
        *,
        choices: set[FT] | None = Undefined,
        readonly: bool = False,
        required: bool = False,
        **kwargs,
    ) -> None:
        if 'field_type' in kwargs:
            self.field_type = kwargs.pop('field_type')

        if 'field_name' in kwargs:
            self.field_name = kwargs.pop('field_name')

        if choices:
            self.choices = choices
        else:
            self.choices = set()

        # We always validate the default value
        if default != self.default:
            # Use class here to avoid issues with subclasses, BaseMapping as example
            if default.__class__ in (dict, list, set) and not readonly:
                # https://docs.astral.sh/ruff/rules/mutable-class-default/
                # For default values they can't be mutable types unless the field is readonly
                raise ValueError('Default value cannot be a dict, list, or set unless the field is readonly.')

            default = self.clean_value(default)
            self.validate(default)

        self.default = default
        ## These must be after default validation otherwise we can't set readonly fields with default values
        ## and then we can't set required fields with default even NONE values
        self.readonly = readonly
        self.required = required

        if kwargs:
            for key, value in kwargs.items():
                setattr(self, key, value)

    def __delete__(self, obj: 'BaseMapping') -> None:
        """
        Delete the attribute/key from the instance.

        Args:
            obj (BaseMapping): The instance from which to delete the attribute.
        """
        if self.field_name in obj.__dict__:
            del obj.__dict__[self.field_name]

        if self.field_name in obj:
            del obj[self.field_name]

        obj.__deleted_keys__.add(self.field_name)

    def __get__(self, obj: 'BaseMapping', cls: type) -> FT:
        """
        Get the attribute/key from the instance or from the class.

        Args:
            obj (BaseMapping): The instance from which to get the attribute.
            cls (type): The class of the instance.

        Raises:
            AttributeError: If the attribute is not found in the instance.
        """
        # When obj is None, it means we are accessing the descriptor from the class, not from an instance
        if obj is None:
            return self.default

        if self.field_name in obj:
            return obj[self.field_name]

        if self.field_name in obj.__dict__:
            return obj.__dict__[self.field_name]

        if self.field_name not in obj.__deleted_keys__:
            return self.default

        msg = f"'{cls.__name__}' object has no attribute '{self.field_name}'."
        raise AttributeError(msg) from None

    def __set__(self, obj: 'BaseMapping', value: FT) -> None:
        """
        Set the attribute/key in the instance.

        Args:
            obj (BaseMapping): The instance in which to set the attribute.
            value (FT): The value to set for the attribute.
        """
        value = self.clean_value(value)
        validate_fields = getattr(obj, '__validate_fields__', False)
        if validate_fields:
            self.validate(value)

        if obj._is_valid_key(self.field_name):  # noqa: SLF001
            obj[self.field_name] = value

        # This makes attribute inheritance work correctly
        elif value != self.default:
            obj.__dict__[self.field_name] = value

    def __set_name__(self, cls: 'BaseMapping', name: str) -> None:
        """
        Set the name of the field in the descriptor.
        It's called when the class is created in the Python runtime.

        Args:
            cls (BaseMapping): The class where the descriptor is used.
            name (str): The name of the attribute in the class.
        """
        if self.field_name is None:
            self.field_name = name

    ## Private methods
    def _get_choices(self) -> set[FT]:
        """Get the choices for the field."""
        return self.choices if self.choices is not None else set()

    def _validate_choices(self, value: FT) -> None:
        """
        Validate that the value is one of the allowed choices.

        Args:
            value (FT): The value to validate.

        Raises:
            ValueError: If the value is not one of the allowed choices.
        """
        # None and Undefined are always allowed
        if value is None or value is Undefined:
            return

        choices = self._get_choices()
        msg = f"The value '{value}' for field '{self.field_name}' must be in this list {choices}."
        if isinstance(value, set):
            if value.difference(choices):
                raise ValueError(msg)

            return

        if isinstance(value, (list, tuple)):
            if set(value).difference(choices):
                raise ValueError(msg)
            return

        if value not in choices:
            raise ValueError(msg)

    def _validate_instance(self, value: FT) -> None:
        """
        Validate that the value is an instance of the expected field type.

        Args:
            value (FT): The value to validate.

        Raises:
            TypeError: If the value is not an instance of the expected field type.
        """
        if value and not isinstance(value, self.field_type):
            name = self.field_name
            type_name = self.field_type.__qualname__
            received_name = type(value).__qualname__
            msg = f'Field {name} must be of type {type_name}, got {received_name}.'
            raise TypeError(msg)

    def _validate_max_value(self, value: FT, max_value: FT, msg: str | None = None) -> None:
        """
        Validate that the value is less than or equal to the maximum value.

        Args:
            value (FT): The value to validate.
            max_value (FT): The maximum allowed value.
            msg (str | None): Custom error message.

        Raises:
            ValueError: If the value is greater than the maximum allowed value.
        """
        if value is not None and value is not Undefined:
            max_value = max_value if not callable(max_value) else max_value()
            if msg is None:
                msg = f"The value '{value}' for field '{self.field_name}' must be less than or equal to {max_value}."

            if value > max_value:
                raise ValueError(msg)

    def _validate_min_value(self, value: FT, min_value: FT, msg: str | None = None) -> None:
        """
        Validate that the value is greater than or equal to the minimum value.

        Args:
            value (FT): The value to validate.
            min_value (FT): The minimum allowed value.
            msg (str | None): Custom error message.

        Raises:
            ValueError: If the value is less than the minimum allowed value.
        """
        if value is not None and value is not Undefined:
            min_value = min_value if not callable(min_value) else min_value()
            if msg is None:
                msg = f"The value '{value}' for field '{self.field_name}' must be greater than or equal to {min_value}."

            if value < min_value:
                raise ValueError(msg)

    def _validate_readonly(self, value: FT) -> None:  # noqa: ARG002
        """
        Validate that the value is readonly.

        Args:
            value (FT): The value to validate.

        Raises:
            ValueError: If the value is readonly.
        """
        msg = f'Field {self.field_name} is readonly.'
        raise ReadonlyError(msg)

    def _validate_required(self, value: FT) -> None:
        """
        Validate that the value is required.

        Args:
            value (FT): The value to validate.

        Raises:
            ValueError: If the value is required.
        """
        if not value and value not in (0, 0.0, False):
            msg = f'Field {self.field_name} is required.'
            raise RequiredError(msg)

    ## Public methods
    def clean_value(self, value: object) -> FT:
        """
        Clean the value before setting it in the instance.
        It can be overridden in subclasses to implement custom cleaning logic.
        Like string to Date/DateTime objects, etc.

        Args:
            value (object): The value assigned to the field.
        """
        return value

    def validate(self, value: object) -> None:
        """
        It's called to validate the value before setting it in the instance.
        It can be overridden in subclasses to implement custom validation logic.

        Args:
            value (object): The value assigned to the field.

        Raises:
            ValueError: If the value is not valid.
        """
        # We need to validate required first because None and Undefined
        # could be seen and are not valid choices for this validation
        if self.required:
            self._validate_required(value=value)

        # We only validate if the value is different from default
        # This allows us to set the default value in the initialization
        if value != self.default:
            if self.readonly:
                self._validate_readonly(value=value)

            if self.choices or self._get_choices():
                self._validate_choices(value=value)

            if self.field_type:
                self._validate_instance(value=value)


## StrField is here because other fields like ChoiceField/EmailField/URLField
## inherit from it otherwise we keep alphabetical order
class StrField(BaseMappingField[str]):
    field_type: type = str
    min_size: int = 0
    max_size: int = max_int
    regex: str | None = None

    def __init__(
        self,
        default: FT | None = Undefined,
        *,
        choices: set[FT] | None = Undefined,
        min_size: int | None = None,
        max_size: int | None = None,
        regex: str | None = None,
        readonly: bool = False,
        required: bool = False,
        **kwargs,
    ) -> None:
        if min_size is not None:
            self.min_size = min_size

        if max_size is not None:
            self.max_size = max_size

        if regex is not None:
            self.regex = regex

        super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)

    def validate(self, value: object) -> None:
        """
        Validate the value against the regex pattern if provided.

        Args:
            value (object): The value assigned to the field.

        Raises:
            ValueError: If the value does not match the regex pattern.
        """
        super().validate(value)

        if self.regex and isinstance(value, str) and not re.match(self.regex, value):
            msg = f"The value '{value}' for field '{self.field_name}' must match with this regex: {self.regex}"
            raise ValueError(msg)

        if value is not None and value is not Undefined:
            actual_size = len(value)
            if self.min_size is not None:
                msg = f"The size for field '{self.field_name}' must be greater than or equal to {self.min_size}."
                self._validate_min_value(value=actual_size, min_value=self.min_size, msg=msg)

            if self.max_size is not None:
                msg = f"The size for field '{self.field_name}' must be less than or equal to {self.max_size}."
                self._validate_max_value(value=actual_size, max_value=self.max_size, msg=msg)


class BoolField(BaseMappingField[bool]):
    field_type: type = bool

    def clean_value(self, value: object) -> bool:
        """
        Converts the  given value to a boolean if possible using the 'everysk.utils.bool_convert' function.

        Args:
            value (object): The value assigned to the field.

        Returns:
            Any: The value converted to its boolean corresponding

        Example:
            >>> from everysk.core.mapping import BoolField
            >>> bool_field = BoolField()
            >>> bool_field.clean_value("y")
            >>> True

            >>> bool_field.clean_value("n")
            >>> False

            >>> bool_field.clean_value("a")
            >>> ValueError: Invalid truth value 'a'
        """
        # https://docs.python.org/3.9/distutils/apiref.html#distutils.util.strtobool
        # The module distutils is deprecated, then we put the function code inside our codebase
        if value is not None and value is not Undefined:
            # If the format is invalid, we let the validation handle the error
            with contextlib.suppress(ValueError):
                value = bool_convert(value)

        return super().clean_value(value)


class ChoiceField(StrField):
    # Legacy class for compatibility
    pass


class DateField(BaseMappingField[Date]):
    field_type: type = Date
    min_date: Date | Callable = None
    max_date: Date | Callable = None

    def __init__(
        self,
        default: FT | None = Undefined,
        *,
        choices: set[FT] | None = Undefined,
        min_date: Date | Callable = None,
        max_date: Date | Callable = None,
        readonly: bool = False,
        required: bool = False,
        **kwargs,
    ) -> None:
        if min_date is not None:
            self.min_date = min_date

        if max_date is not None:
            self.max_date = max_date

        super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)

    def clean_value(self, value: object) -> Date:
        """
        Convert a string representation of a date into a Date object.

        Args:
            value (object): The value assigned to the field.

        Raises:
            ValueError: If the string is not represented in either
                        ISO format ("YYYY-MM-DD") or Everysk format ("YYYYMMDD").

        Example:
            >>> from everysk.core.fields import DateField
            >>> date_field = DateField()
            >>> date_field.clean_value("20140314")
            Date(2014, 3, 14)
            >>> date_field.clean_value("2014-03-14")
            Date(2014, 3, 14)
        """
        if isinstance(value, str):
            if '-' in value:
                value = Date.fromisoformat(value)
            else:
                # Everysk format
                # If the format is invalid, we let the validation handle the error
                with contextlib.suppress(ValueError):
                    value = Date.strptime(value, '%Y%m%d')

        # Ensure that Date and python date objects are converted correctly
        elif Date.is_date(value):
            value = Date.fromisoformat(value.isoformat())

        return super().clean_value(value)

    def validate(self, value: object) -> None:
        """
        Validate that the date is within the specified min_date and max_date if provided.

        Args:
            value (object): The value assigned to the field.

        Raises:
            ValueError: If the date is outside the specified range.
        """
        super().validate(value)

        if self.min_date is not None:
            self._validate_min_value(value=value, min_value=self.min_date)

        if self.max_date is not None:
            self._validate_max_value(value=value, max_value=self.max_date)


class DateTimeField(BaseMappingField[DateTime]):
    field_type: type = DateTime
    force_time: Literal['MIDDAY', 'NOW', 'FIRST_MINUTE', 'LAST_MINUTE'] = None
    min_date: DateTime | Callable = None
    max_date: DateTime | Callable = None

    def __init__(
        self,
        default: FT | None = Undefined,
        *,
        choices: set[FT] | None = Undefined,
        force_time: Literal['MIDDAY', 'NOW', 'FIRST_MINUTE', 'LAST_MINUTE'] | None = None,
        min_date: DateTime | Callable = None,
        max_date: DateTime | Callable = None,
        readonly: bool = False,
        required: bool = False,
        **kwargs,
    ) -> None:
        if force_time is not None:
            self.force_time = force_time

        if min_date is not None:
            self.min_date = min_date

        if max_date is not None:
            self.max_date = max_date

        super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)

    def clean_value(self, value: object) -> DateTime:
        """
        Convert a string representation of a datetime into a DateTime object.

        Args:
            value (object): The value assigned to the field.

        Raises:
            ValueError: If the string is not represented in either
                        ISO format ("YYYY-MM-DDTHH:MM:SS") or Everysk format ("YYYYMMDD HHMMSS").

        Example:
            >>> from everysk.core.fields import DateTimeField
            >>> datetime_field = DateTimeField()
            >>> datetime_field.clean_value("20140314 153000")
            DateTime(2014, 3, 14, 15, 30, 0)
            >>> datetime_field.clean_value("2014-03-14T15:30:00")
            DateTime(2014, 3, 14, 15, 30, 0)
        """
        if isinstance(value, str):
            # If the format is invalid, we let the validation handle the error
            with contextlib.suppress(ValueError):
                value: DateTime = DateTime.fromisoformat(value)

        # Ensure that Date/DateTime and python date/datetime objects are converted correctly
        elif Date.is_date(value) or DateTime.is_datetime(value):
            value: DateTime = DateTime.fromisoformat(value.isoformat())

        if self.force_time and DateTime.is_datetime(value):
            value = value.force_time(self.force_time)

        return super().clean_value(value)

    def validate(self, value: object) -> None:
        """
        Validate that the datetime is within the specified min_date and max_date if provided.

        Args:
            value (object): The value assigned to the field.

        Raises:
            ValueError: If the datetime is outside the specified range.
        """
        super().validate(value)

        if self.min_date is not None:
            self._validate_min_value(value=value, min_value=self.min_date)

        if self.max_date is not None:
            self._validate_max_value(value=value, max_value=self.max_date)


class DictField(BaseMappingField[dict]):
    field_type: type = dict

    class ReadonlyDict(dict):
        __setitem__ = _do_nothing
        __delitem__ = _do_nothing
        pop = _do_nothing
        popitem = _do_nothing
        clear = _do_nothing
        update = _do_nothing
        setdefault = _do_nothing

    def clean_value(self, value: object) -> dict | ReadonlyDict:
        """
        If the field is readonly, convert the given dict into a ReadonlyDict.

        Args:
            value (object): The value assigned to the field.
        """
        if self.readonly and isinstance(value, dict):
            value = self.ReadonlyDict(value)

        return super().clean_value(value)


class EmailField(StrField):
    field_type: type = str
    min_size: int = 5
    max_size: int = 320

    def validate(self, value: object) -> None:
        """
        Validates if the value is an e-mail address.
        To validate we check the existence of the '@' character and the length of the string.

        Args:
            value (object): The value assigned to the field.

        Raises:
            ValueError: If the value is not an e-mail address.
        """
        super().validate(value)

        # The maximum length of an email is 320 characters per RFC 3696 section 3 and at least 5 digits a@b.c
        msg = f'Key {self.field_name} must be an e-mail.'
        if value is not None and value is not Undefined:
            if '@' not in value:
                raise ValueError(msg)

            try:
                # Get the cases where we could have more than one @ in the string
                user, domain = value.split('@')  # noqa: RUF059
            except ValueError as error:
                raise ValueError(msg) from error


class FloatField(BaseMappingField[float]):
    field_type: type = float
    min_value: float = float('-inf')
    max_value: float = float('inf')

    def __init__(
        self,
        default: FT | None = Undefined,
        *,
        choices: set[FT] | None = Undefined,
        min_value: float | None = None,
        max_value: float | None = None,
        readonly: bool = False,
        required: bool = False,
        **kwargs,
    ) -> None:
        if min_value is not None:
            self.min_value = min_value

        if max_value is not None:
            self.max_value = max_value

        super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)

    def clean_value(self, value: object) -> float:
        """
        Convert a string or integer representation of a float into a float object.

        Args:
            value (object): The value assigned to the field.

        Raises:
            ValueError: If value could not be converted to a float.
        """
        # Convert Float strings to float object
        if isinstance(value, (int, str)):
            # If the format is invalid, we let the validation handle the error
            with contextlib.suppress(ValueError):
                value = float(value)

        return super().clean_value(value)

    def validate(self, value: object) -> None:
        """
        Validate that the float value is within the specified min_value and max_value.

        Args:
            value (object): The value assigned to the field.

        Raises:
            ValueError: If the float value is outside the specified range.
        """
        super().validate(value)

        if self.min_value is not None:
            self._validate_min_value(value=value, min_value=self.min_value)

        if self.max_value is not None:
            self._validate_max_value(value=value, max_value=self.max_value)


class IntField(BaseMappingField[int]):
    field_type: type = int
    min_value: int = -max_int - 1
    max_value: int = max_int

    def __init__(
        self,
        default: FT | None = Undefined,
        *,
        choices: set[FT] | None = Undefined,
        min_value: int | None = None,
        max_value: int | None = None,
        readonly: bool = False,
        required: bool = False,
        **kwargs,
    ) -> None:
        if min_value is not None:
            self.min_value = min_value

        if max_value is not None:
            self.max_value = max_value

        super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)

    def clean_value(self, value: object) -> int:
        """
        Convert a string or float representation of an integer into an int object.

        Args:
            value (object): The value assigned to the field.

        Raises:
            ValueError: If value could not be converted to an int.
        """
        # Convert Int strings to int object
        if isinstance(value, str):
            # If the format is invalid, we let the validation handle the error
            with contextlib.suppress(ValueError):
                value = int(value)

        return super().clean_value(value)

    def validate(self, value: object) -> None:
        """
        Validate that the integer value is within the specified min_value and max_value.

        Args:
            value (object): The value assigned to the field.

        Raises:
            ValueError: If the integer value is outside the specified range.
        """
        super().validate(value)

        if self.min_value is not None:
            self._validate_min_value(value=value, min_value=self.min_value)

        if self.max_value is not None:
            self._validate_max_value(value=value, max_value=self.max_value)


class IteratorField(BaseMappingField[Iterator]):
    field_type: type = Iterator
    separator: str = ','

    def __init__(
        self,
        default: FT | None = Undefined,
        *,
        choices: set[FT] | None = Undefined,
        readonly: bool = False,
        required: bool = False,
        separator: str = ',',
        **kwargs,
    ) -> None:
        self.separator = separator
        super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)

    def clean_value(self, value: object) -> Iterator:
        # Convert List/Set/Tuple to iterator
        if isinstance(value, (list, set, tuple)):
            value = iter(value)

        elif isinstance(value, str) and self.separator in value:
            value = iter([item.strip() for item in value.split(self.separator)])

        return super().clean_value(value)


class ListField(BaseMappingField[list]):
    """
    https://stackoverflow.com/questions/855191/how-big-can-a-python-list-get#comment112727918_15739630
    Sys.maxsize should give you 2^31 - 1 on a 32-bit platform and 2^63 - 1 on a 64-bit platform.
    However, because each pointer on the 32 bit list takes up 4 bytes or on a 64 bit it's 8 bytes,
    Python would give you an error if you attempt to make a list larger than maxsize/8 on a 64-bit
    system or maxsize/4 on a 32-bit system.

    Example:
        >>> import sys
        >>> sys.maxsize
        9223372036854775807
        >>> sys.maxsize // 8
        1152921504606846975 # Maximum list size on a 64-bit system that is pretty much impossible to reach
    """

    field_type: type = list
    min_size: int = 0
    max_size: int = max_int // 8
    separator: str = ','

    class ReadonlyList(list):
        append = _do_nothing
        clear = _do_nothing
        extend = _do_nothing
        insert = _do_nothing
        pop = _do_nothing
        remove = _do_nothing
        reverse = _do_nothing
        sort = _do_nothing

    def __init__(
        self,
        default: FT | None = Undefined,
        *,
        choices: set[FT] | None = Undefined,
        min_size: int | None = None,
        max_size: int | None = None,
        readonly: bool = False,
        required: bool = False,
        separator: str = ',',
        **kwargs,
    ) -> None:
        if min_size is not None:
            self.min_size = min_size

        if max_size is not None:
            self.max_size = max_size

        self.separator = separator
        super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)

    def clean_value(self, value: object) -> list | ReadonlyList:
        """
        Convert Tuple/Set to List.
        Convert a string that represents a list into a list object using the defined separator.
        If the field is readonly, convert the given list into a ReadonlyList.

        Args:
            value (object): The value assigned to the field.
        """
        if isinstance(value, (tuple, set)):
            value = list(value)

        elif isinstance(value, str) and self.separator in value:
            value = [item.strip() for item in value.split(self.separator)]

        if self.readonly and isinstance(value, list):
            value = self.ReadonlyList(value)

        return super().clean_value(value)

    def validate(self, value: object) -> None:
        """
        Validate that the list size is within the specified min_size and max_size.

        Args:
            value (object): The value assigned to the field.

        Raises:
            ValueError: If the list size is outside the specified range.
        """
        super().validate(value)

        if value is not None and value is not Undefined:
            actual_size = len(value)
            if self.min_size is not None:
                msg = f"The size for field '{self.field_name}' must be greater than or equal to {self.min_size}."
                self._validate_min_value(value=actual_size, min_value=self.min_size, msg=msg)

            if self.max_size is not None:
                msg = f"The size for field '{self.field_name}' must be less than or equal to {self.max_size}."
                self._validate_max_value(value=actual_size, max_value=self.max_size, msg=msg)


class SetField(BaseMappingField[set]):
    field_type: type = set
    min_size: int = 0
    max_size: int = max_int // 8
    separator: str = ','

    class ReadonlySet(set):
        add = _do_nothing
        clear = _do_nothing
        discard = _do_nothing
        pop = _do_nothing
        remove = _do_nothing
        update = _do_nothing

    def __init__(
        self,
        default: FT | None = Undefined,
        *,
        choices: set[FT] | None = Undefined,
        min_size: int | None = None,
        max_size: int | None = None,
        readonly: bool = False,
        required: bool = False,
        separator: str = ',',
        **kwargs,
    ) -> None:
        if min_size is not None:
            self.min_size = min_size

        if max_size is not None:
            self.max_size = max_size

        self.separator = separator
        super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)

    def clean_value(self, value: object) -> set | ReadonlySet:
        """
        Convert List/Tuple to Set.
        Convert a string that represents a set into a set object using the defined separator.
        If the field is readonly, convert the given set into a ReadonlySet.

        Args:
            value (object): The value assigned to the field.
        """
        if isinstance(value, (list, tuple)):
            value = set(value)

        elif isinstance(value, str) and self.separator in value:
            value = {item.strip() for item in value.split(self.separator)}

        if self.readonly and isinstance(value, set):
            value = self.ReadonlySet(value)

        return super().clean_value(value)

    def validate(self, value: object) -> None:
        """
        Validate that the set size is within the specified min_size and max_size.

        Args:
            value (object): The value assigned to the field.

        Raises:
            ValueError: If the set size is outside the specified range.
        """
        super().validate(value)

        if value is not None and value is not Undefined:
            actual_size = len(value)
            if self.min_size is not None:
                msg = f"The size for field '{self.field_name}' must be greater than or equal to {self.min_size}."
                self._validate_min_value(value=actual_size, min_value=self.min_size, msg=msg)

            if self.max_size is not None:
                msg = f"The size for field '{self.field_name}' must be less than or equal to {self.max_size}."
                self._validate_max_value(value=actual_size, max_value=self.max_size, msg=msg)


class RegexField(BaseMappingField[re.Pattern]):
    field_type: type = re.Pattern

    def clean_value(self, value: object) -> re.Pattern:
        """
        Compile the given string into a regex Pattern object.

        Args:
            value (object): The value assigned to the field.
        """
        if isinstance(value, str):
            value = re.compile(value)

        return super().clean_value(value)


class TupleField(BaseMappingField[tuple]):
    field_type: type = tuple
    min_size: int = 0
    max_size: int = max_int // 8
    separator: str = ','

    def __init__(
        self,
        default: FT | None = Undefined,
        *,
        choices: set[FT] | None = Undefined,
        min_size: int | None = None,
        max_size: int | None = None,
        readonly: bool = False,
        required: bool = False,
        separator: str = ',',
        **kwargs,
    ) -> None:
        if min_size is not None:
            self.min_size = min_size

        if max_size is not None:
            self.max_size = max_size

        self.separator = separator
        super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)

    def clean_value(self, value: object) -> tuple:
        """
        Convert a list or set representation of a tuple into a tuple object.
        Convert a string that represents a tuple into a tuple object using the defined separator.

        Args:
            value (object): The value assigned to the field.
        """
        if isinstance(value, (list, set)):
            value = tuple(value)
        elif isinstance(value, str) and self.separator in value:
            value = tuple(item.strip() for item in value.split(self.separator))

        return super().clean_value(value)

    def validate(self, value: object) -> None:
        """
        Validate that the tuple size is within the specified min_size and max_size.

        Args:
            value (object): The value assigned to the field.

        Raises:
            ValueError: If the tuple size is outside the specified range.
        """
        super().validate(value)

        if value is not None and value is not Undefined:
            actual_size = len(value)
            if self.min_size is not None:
                msg = f"The size for field '{self.field_name}' must be greater than or equal to {self.min_size}."
                self._validate_min_value(value=actual_size, min_value=self.min_size, msg=msg)

            if self.max_size is not None:
                msg = f"The size for field '{self.field_name}' must be less than or equal to {self.max_size}."
                self._validate_max_value(value=actual_size, max_value=self.max_size, msg=msg)


class URLField(StrField):
    field_type: type = str
    supported_schemes: set = ('http', 'https', 'ftp', 'ftps', 'git')

    def validate(self, value: object) -> None:
        """
        Validates a URL string to ensure it is a valid URL.
        We check the protocol HTTP, HTTPS, FTP, FTPS, the domain format and size and the port number.

        Args:
            self.field_name (str): The name of the attribute being validated, used for identification in error messages.
            value (Any): The value to be validated for the specified attribute.
            attr_type (type | UnionType, optional):
                The expected type of the attribute. If provided, the value is checked to ensure it is of this type.
                Defaults to None.

        Raises:
            FieldValueError: If the url is invalid.
        """
        super().validate(value)

        if isinstance(value, str):
            # https://github.com/django/django/blob/main/django/core/validators.py
            ul = '\u00a1-\uffff'  # Unicode letters range (must not be a raw string).

            # IP patterns
            ipv4_re = (
                r'(?:0|25[0-5]|2[0-4][0-9]|1[0-9]?[0-9]?|[1-9][0-9]?)'
                r'(?:\.(?:0|25[0-5]|2[0-4][0-9]|1[0-9]?[0-9]?|[1-9][0-9]?)){3}'
            )
            ipv6_re = r'\[[0-9a-f:.]+\]'  # (simple regex, validated later)

            # Host patterns
            hostname_re = r'[a-z' + ul + r'0-9](?:[a-z' + ul + r'0-9-]{0,61}[a-z' + ul + r'0-9])?'

            # Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1
            domain_re = r'(?:\.(?!-)[a-z' + ul + r'0-9-]{1,63}(?<!-))*'
            tld_re = (
                r'\.'  # dot
                r'(?!-)'  # can't start with a dash
                r'(?:[a-z' + ul + '-]{2,63}'  # domain label
                r'|xn--[a-z0-9]{1,59})'  # or punycode label
                r'(?<!-)'  # can't end with a dash
                r'\.?'  # may have a trailing dot
            )
            host_re = '(' + hostname_re + domain_re + tld_re + '|localhost)'

            regex = re.compile(
                r'^(?:[a-z0-9.+-]*)://'  # scheme is validated separately
                r'(?:[^\s:@/]+(?::[^\s:@/]*)?@)?'  # user:pass authentication
                r'(?:' + ipv4_re + '|' + ipv6_re + '|' + host_re + ')'
                r'(?::[0-9]{1,5})?'  # port
                r'(?:[/?#][^\s]*)?'  # resource path
                r'\Z',
                re.IGNORECASE,
            )
            msg = f'Key {self.field_name} must be an URL.'
            if not regex.match(value):
                raise ValueError(msg)

            # The scheme needs a separate check
            try:
                result = urlparse(value)

            except ValueError as error:
                raise ValueError(msg) from error

            if result.scheme not in self.supported_schemes:
                raise ValueError(msg)
