import dataclasses
import typing
from datetime import date
from typing import Any, Dict, Type, TypeVar

T = TypeVar("T")


def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
    """
    Recursively convert a plain dict (e.g. from json.loads) into the given
    dataclass type, using the field type hints to convert nested structures.

    Handles:
    - Nested dataclasses
    - List[SomeDataclass]
    - Optional[X] / Union[X, None]
    - Union[DataclassType, primitive] (e.g. Union[Measurement, int])
    - Primitive pass-through for Union[str, int] etc.
    """
    return _from_dict_impl(cls, data)  # type: ignore[return-value]


def _from_dict_impl(cls: Any, data: Any) -> Any:
    hints = typing.get_type_hints(cls)
    kwargs: Dict[str, Any] = {}

    for field in dataclasses.fields(cls):  # type: ignore[arg-type]
        has_default = (
            field.default is not dataclasses.MISSING
            or field.default_factory is not dataclasses.MISSING  # type: ignore[misc]
        )
        if field.name not in data:
            if has_default:
                continue
            raise ValueError(f"{cls.__name__}: missing required field '{field.name}'")

        kwargs[field.name] = _coerce(data[field.name], hints[field.name])

    return cls(**kwargs)


def _coerce(value: Any, hint: Any) -> Any:
    if value is None:
        return None

    origin = typing.get_origin(hint)
    args = typing.get_args(hint)

    # Union (includes Optional[X] which is Union[X, None])
    if origin is typing.Union:
        if value is None:
            return None
        non_none_args = [a for a in args if a is not type(None)]
        if len(non_none_args) == 1:
            # Optional[X] — recurse so List[X] and nested dataclasses are handled
            return _coerce(value, non_none_args[0])
        # Multi-type Union (e.g. Union[Measurement, int]): try dataclasses first
        for arg in non_none_args:
            if dataclasses.is_dataclass(arg) and isinstance(value, dict):
                return _from_dict_impl(arg, value)
        # All remaining args are primitives — return value as-is
        return value

    # List[X]
    if origin is list:
        item_hint = args[0]
        return [_coerce(item, item_hint) for item in value]

    # Plain dataclass
    if dataclasses.is_dataclass(hint) and isinstance(value, dict):
        return _from_dict_impl(hint, value)

    if hint is date and isinstance(value, str):
        return date.fromisoformat(value)

    return value
