"""Pydantic schema source — validates payloads against a Pydantic model.

See: ``DESIGN.md`` §5 + §14 (``schema/pydantic.py`` is v0.1, v0).

The implementation wraps :class:`pydantic.BaseModel.model_validate`,
unpacking the resulting :class:`pydantic.ValidationError` into a
list of :class:`SchemaValidationIssue` objects. One Pydantic error =
one issue.
"""

from __future__ import annotations

from typing import Any

from pydantic import BaseModel, ValidationError

from mgf.apiprobe.exceptions import SchemaError
from mgf.apiprobe.schema.base import SchemaSource, SchemaValidationIssue


class PydanticSchemaSource(SchemaSource):
    """Validates payloads against a Pydantic model.

    Construct with a :class:`pydantic.BaseModel` subclass::

        from pydantic import BaseModel

        class UserResponse(BaseModel):
            id: str
            email: str

        schema = PydanticSchemaSource(UserResponse)
        issues = schema.validate({"id": "abc", "email": "x@y.z"})
        assert issues == []
    """

    def __init__(self, model: type[BaseModel]) -> None:
        if not isinstance(model, type) or not issubclass(model, BaseModel):
            raise SchemaError(
                f"PydanticSchemaSource requires a pydantic.BaseModel subclass; got {model!r}"
            )
        self.model = model

    def validate(self, payload: Any) -> list[SchemaValidationIssue]:
        try:
            self.model.model_validate(payload)
        except ValidationError as exc:
            return [_to_issue(dict(err)) for err in exc.errors()]
        return []

    def describe(self) -> str:
        return f"pydantic:{self.model.__module__}.{self.model.__qualname__}"


def _to_issue(err: dict[str, Any]) -> SchemaValidationIssue:
    """Convert one entry from ``ValidationError.errors()`` to an issue."""
    loc = err.get("loc", ())
    path = "$" if not loc else "$." + ".".join(str(x) for x in loc)
    return SchemaValidationIssue(
        path=path,
        message=str(err.get("msg", "")),
        expected=str(err.get("type", "")),
        actual=_format_actual(err.get("input")),
    )


def _format_actual(value: Any) -> str:
    """Render the actual value compactly for Finding evidence."""
    if value is None:
        return "null"
    if isinstance(value, bool):
        return f"bool {value}"
    if isinstance(value, (int, float)):
        return f"{type(value).__name__} {value}"
    if isinstance(value, str):
        # Cap length so noisy bodies don't blow up reports.
        snippet = value if len(value) <= 64 else value[:61] + "..."
        return f"str {snippet!r}"
    return f"{type(value).__name__}"


__all__ = ["PydanticSchemaSource"]
