"""Validacion de dicts contra un subset de JSON Schema (draft 2020-12) sin dependencias externas."""

import re


def validate_json_schema(data: dict, schema: dict) -> tuple[bool, list[str]]:
    """Valida un dict contra un schema JSON Schema (draft 2020-12).

    Soporta: type, required, properties, items, minItems/maxItems,
    minimum/maximum, exclusiveMinimum/exclusiveMaximum,
    minLength/maxLength, pattern, enum.

    Args:
        data: Datos a validar.
        schema: Schema JSON Schema (draft 2020-12) como dict.

    Returns:
        (valid, errors) — True y lista vacia si cumple, False y lista de
        errores descriptivos si no. Cada error incluye el path (ej: "$.address.zip").
    """
    errors: list[str] = []
    _validate(data, schema, "$", errors)
    return (len(errors) == 0, errors)


_TYPE_CHECKS = {
    "string": lambda v: isinstance(v, str),
    "number": lambda v: isinstance(v, (int, float)) and not isinstance(v, bool),
    "integer": lambda v: isinstance(v, int) and not isinstance(v, bool),
    "boolean": lambda v: isinstance(v, bool),
    "array": lambda v: isinstance(v, list),
    "object": lambda v: isinstance(v, dict),
    "null": lambda v: v is None,
}

_TYPE_NAMES = {
    str: "string",
    int: "integer",
    float: "number",
    bool: "boolean",
    list: "array",
    dict: "object",
    type(None): "null",
}


def _type_name(v: object) -> str:
    return _TYPE_NAMES.get(type(v), type(v).__name__)


def _validate(data: object, schema: dict, path: str, errors: list[str]) -> None:
    if not schema:
        return

    # --- type ---
    expected_type = schema.get("type")
    if expected_type is not None:
        checker = _TYPE_CHECKS.get(expected_type)
        if checker and not checker(data):
            errors.append(
                f"{path}: expected {expected_type}, got {_type_name(data)}"
            )
            # no seguir validando si el tipo base ya fallo
            return

    # --- enum ---
    if "enum" in schema:
        if data not in schema["enum"]:
            errors.append(
                f"{path}: value {data!r} not in enum {schema['enum']}"
            )

    # --- object ---
    if isinstance(data, dict):
        required = schema.get("required", [])
        for field in required:
            if field not in data or data[field] is None or data[field] == "":
                errors.append(f"{path}.{field}: required field missing or empty")

        properties = schema.get("properties", {})
        for field, sub_schema in properties.items():
            if field in data:
                _validate(data[field], sub_schema, f"{path}.{field}", errors)

    # --- array ---
    elif isinstance(data, list):
        min_items = schema.get("minItems")
        max_items = schema.get("maxItems")
        if min_items is not None and len(data) < min_items:
            errors.append(
                f"{path}: array length {len(data)} < minItems {min_items}"
            )
        if max_items is not None and len(data) > max_items:
            errors.append(
                f"{path}: array length {len(data)} > maxItems {max_items}"
            )

        items_schema = schema.get("items")
        if items_schema:
            for i, item in enumerate(data):
                _validate(item, items_schema, f"{path}[{i}]", errors)

    # --- string ---
    elif isinstance(data, str):
        min_len = schema.get("minLength")
        max_len = schema.get("maxLength")
        if min_len is not None and len(data) < min_len:
            errors.append(
                f"{path}: string length {len(data)} < minLength {min_len}"
            )
        if max_len is not None and len(data) > max_len:
            errors.append(
                f"{path}: string length {len(data)} > maxLength {max_len}"
            )
        pattern = schema.get("pattern")
        if pattern is not None and not re.search(pattern, data):
            errors.append(
                f"{path}: value {data!r} does not match pattern {pattern!r}"
            )

    # --- number / integer ---
    elif isinstance(data, (int, float)) and not isinstance(data, bool):
        minimum = schema.get("minimum")
        maximum = schema.get("maximum")
        ex_min = schema.get("exclusiveMinimum")
        ex_max = schema.get("exclusiveMaximum")
        if minimum is not None and data < minimum:
            errors.append(f"{path}: {data} < minimum {minimum}")
        if maximum is not None and data > maximum:
            errors.append(f"{path}: {data} > maximum {maximum}")
        if ex_min is not None and data <= ex_min:
            errors.append(f"{path}: {data} <= exclusiveMinimum {ex_min}")
        if ex_max is not None and data >= ex_max:
            errors.append(f"{path}: {data} >= exclusiveMaximum {ex_max}")
