from __future__ import annotations

import inspect
import typing as t

T = t.TypeVar("T", bound="Serializable")


class Serializable:
    _EXCLUDE_VARS: t.ClassVar[tuple[str, ...]] = ("self", "extra")
    extra: dict[str, t.Any]

    @classmethod
    def _supported_fields(cls) -> list[str]:
        signature = inspect.signature(cls.__init__)
        return [
            name
            for name in signature.parameters.keys()
            if name not in cls._EXCLUDE_VARS
        ]

    @classmethod
    def from_dict(cls: type[T], data: dict[str, t.Any]) -> T:
        """
        Instantiate from a dictionary.

        :param data: The dictionary to create the error from.
        :type data: dict
        """

        # Extract any extra fields
        extras = {k: v for k, v in data.items() if k not in cls._supported_fields()}
        kwargs: dict[str, t.Any] = {"extra": extras}
        # Ensure required fields are supplied
        for field_name in cls._supported_fields():
            kwargs[field_name] = data.get(field_name)

        return cls(**kwargs)

    def to_dict(self, include_extra: bool = False) -> dict[str, t.Any]:
        """
        Render to a dictionary.

        :param include_extra: Whether to include stored extra (non-standard) fields in
            the returned dictionary.
        :type include_extra: bool
        """
        result = {}

        # Set any authorization parameters
        for field in self._supported_fields():
            value = getattr(self, field)
            if value is not None:
                if isinstance(value, Serializable):
                    value = value.to_dict(include_extra=include_extra)
                result[field] = value

        # Set any extra fields
        if include_extra:
            result.update(self.extra)

        return result
