"""Schema parser: detects and normalises Pydantic models, JSON Schema dicts, and plain dicts."""

from __future__ import annotations

import json
from typing import Any

from .exceptions import SchemaParseError
from .types import SchemaDefinition, SchemaType

try:
    import jsonschema

    JSONSCHEMA_AVAILABLE = True
except ImportError:
    JSONSCHEMA_AVAILABLE = False

try:
    from pydantic import BaseModel
    from pydantic import ValidationError as PydanticValidationError

    PYDANTIC_AVAILABLE = True
except ImportError:
    BaseModel = None  # type: ignore
    PydanticValidationError = None  # type: ignore
    PYDANTIC_AVAILABLE = False


def detect_schema_type(schema: SchemaDefinition) -> SchemaType:
    """Return the SchemaType for *schema*."""
    if PYDANTIC_AVAILABLE and isinstance(schema, type) and issubclass(schema, BaseModel):
        return SchemaType.PYDANTIC

    if isinstance(schema, dict):
        # Heuristic: JSON Schema objects tend to have $schema / type / properties.
        json_schema_keys = {"$schema", "$id", "type", "properties", "allOf", "anyOf", "oneOf"}
        if json_schema_keys.intersection(schema.keys()):
            return SchemaType.JSON_SCHEMA
        return SchemaType.DICT

    raise SchemaParseError(
        f"Cannot determine schema type for {type(schema).__name__!r}.",
        schema=schema,
    )


def pydantic_schema_to_json(model: type[BaseModel]) -> dict[str, Any]:
    """Return the JSON Schema dict generated by a Pydantic model."""
    if not PYDANTIC_AVAILABLE:
        raise SchemaParseError("Pydantic is not installed.")
    try:
        return model.model_json_schema()
    except AttributeError:
        # Pydantic v1
        return model.schema()


def validate_against_pydantic(
    data: dict[str, Any],
    model: type[BaseModel],
) -> tuple[Any, list[dict[str, Any]]]:
    """
    Validate *data* against a Pydantic *model*.

    Returns (instance, errors).  *errors* is empty on success.
    """
    if not PYDANTIC_AVAILABLE:
        raise SchemaParseError("Pydantic is not installed.")
    try:
        instance = model(**data)
        return instance, []
    except PydanticValidationError as exc:
        errors = _normalise_pydantic_errors(exc)
        return None, errors


def _normalise_pydantic_errors(exc: Any) -> list[dict[str, Any]]:
    """Convert a Pydantic ValidationError into a list of plain dicts."""
    try:
        return list(exc.errors())
    except Exception:
        return [{"msg": str(exc)}]


def validate_against_json_schema(
    data: Any, schema: dict[str, Any]
) -> tuple[Any, list[dict[str, Any]]]:
    """
    Validate *data* against a JSON Schema *schema*.

    Returns (data, errors).  *errors* is empty on success.
    """
    if not JSONSCHEMA_AVAILABLE:
        # Fallback: only check that data is a dict/list when jsonschema isn't
        # present.  This is intentionally lenient.
        return data, []

    validator_cls = jsonschema.Draft7Validator
    validator = validator_cls(schema)
    raw_errors = list(validator.iter_errors(data))
    if not raw_errors:
        return data, []

    errors = [
        {
            "loc": list(err.absolute_path),
            "msg": err.message,
            "type": err.validator,
        }
        for err in raw_errors
    ]
    return None, errors


def validate_against_dict_schema(
    data: dict[str, Any], schema: dict[str, Any]
) -> tuple[Any, list[dict[str, Any]]]:
    """
    Validate *data* against a plain-dict schema (required keys + optional types).

    Schema format::

        {
            "field_name": str | int | float | bool | list | dict,   # type check
            "required_field": ...,                                   # marks as required, any type
        }
    """
    errors: list[dict[str, Any]] = []

    for key, expected_type in schema.items():
        if expected_type is ...:
            # Field is required, no type restriction.
            if key not in data:
                errors.append({"loc": [key], "msg": "field required", "type": "missing"})
        elif key in data and not isinstance(data[key], expected_type):
            errors.append(
                {
                    "loc": [key],
                    "msg": (
                        f"expected {expected_type.__name__!r}, got {type(data[key]).__name__!r}"
                    ),
                    "type": "type_error",
                }
            )
        # Missing optional keys are fine.

    return (data if not errors else None), errors


class SchemaParser:
    """
    Central entry-point that dispatches validation to the right backend.

    Usage::

        parser = SchemaParser(MyPydanticModel)
        instance, errors = parser.validate({"name": "Alice", "age": 30})
    """

    def __init__(self, schema: SchemaDefinition) -> None:
        self.schema = schema
        self.schema_type = detect_schema_type(schema)

    # ------------------------------------------------------------------
    # Public helpers
    # ------------------------------------------------------------------

    @property
    def json_schema(self) -> dict[str, Any]:
        """Return the JSON Schema representation of the underlying schema."""
        if self.schema_type == SchemaType.PYDANTIC:
            return pydantic_schema_to_json(self.schema)  # type: ignore
        if self.schema_type in (SchemaType.JSON_SCHEMA, SchemaType.DICT):
            return self.schema  # type: ignore
        raise SchemaParseError("Cannot produce JSON Schema for this schema type.")

    def describe(self) -> str:
        """Return a human-readable description of the expected output format."""
        try:
            return json.dumps(self.json_schema, indent=2)
        except Exception:
            return str(self.schema)

    # ------------------------------------------------------------------
    # Validation
    # ------------------------------------------------------------------

    def validate(self, data: Any) -> tuple[Any, list[dict[str, Any]]]:
        """
        Validate *data* according to the encapsulated schema.

        Returns ``(result, errors)`` where *errors* is an empty list on success
        and *result* is the validated object (or ``None`` on failure).
        """
        if not isinstance(data, (dict, list)):
            return None, [{"msg": f"Expected a dict or list, got {type(data).__name__!r}"}]

        if self.schema_type == SchemaType.PYDANTIC:
            return validate_against_pydantic(data, self.schema)  # type: ignore

        if self.schema_type == SchemaType.JSON_SCHEMA:
            return validate_against_json_schema(data, self.schema)  # type: ignore

        if self.schema_type == SchemaType.DICT:
            return validate_against_dict_schema(data, self.schema)  # type: ignore

        raise SchemaParseError(f"Unsupported schema type: {self.schema_type!r}")
