from __future__ import annotations

from typing import Union, List, Dict, Tuple, cast, Any, Callable


class _MissingType:
    """A special singleton type - only use for the public global Missing object defined below."""
    def __bool__(self):
        return False

    def __repr__(self):
        return 'Missing'

    def __copy__(self):
        return Missing  # Singleton instance, shall not be copied

    def __deepcopy__(self, _):
        return Missing  # Singleton instance, shall not be copied


# Placeholder value for required fields (i.e., the field doesn't specify a default, nothing to do with Optional).
# Validation will always fail if this value is present, i.e. it's never present in valid models.
# It is a singleton object, no other instances of _MissingType should ever exist.
# In the unlikely event of needing to check for this value in code, use `model.field is Missing`.
Missing = _MissingType()


class StereotypeError(Exception):
    pass


class ConfigurationError(StereotypeError, AssertionError):
    pass


# The validation context is an opaque value passed along to custom Field validators and validate_* methods on Models.
# The type alias serves purely as internal documentation, making types easier to read.
ValidationContextType = Any

# The to_primitive context is an opaque value passed along for use in custom Field and Model implementations.
# The type alias serves purely as internal documentation, making types easier to read.
ToPrimitiveContextType = Any

# An error message (second element) with a path of primitive field names leading to the bad data.
PathErrorType = Tuple[Tuple[str, ...], str]

# Receives the validated value as the only argument, raises ValueError if invalid.
Validator = Callable[[Any, ValidationContextType], None]


class DataError(StereotypeError):
    error_list: List[PathErrorType]

    def __init__(self, errors: List[PathErrorType]):
        self.error_list = errors
        super().__init__(self._error_string())

    def _error_string(self) -> str:
        for path, error in self.error_list:
            if not path:
                return error
            return f'{": ".join(path)}: {error}'
        assert self.error_list, 'Cannot create the exception without any errors'

    @property
    def errors(self) -> Dict[str, Union[List[str], dict]]:
        errors = {}
        for path, error in self.error_list:
            container = errors
            if not path:
                path = ('_global',)
            for item in path[:-1]:
                value = container.setdefault(item, {})
                if isinstance(value, list):
                    value = container[item] = cast(dict, {'_global': value})
                container = value
            array = container.setdefault(path[-1], [])
            array.append(error)
        return errors

    @classmethod
    def new(cls, message: str, *path: str):
        return cls([(path, message)])

    def wrapped(self, *path: str) -> DataError:
        raise type(self)([(path + original_path, error) for original_path, error in self.error_list])


class ValidationError(DataError, ValueError):
    pass


class ConversionError(DataError, TypeError):
    pass
