"""
Schema Validation - Lightweight JSON Schema validation for tool arguments.

Validates action args against tool input schemas without external dependencies.
"""
from typing import Any, Optional, Union


class SchemaValidationError(Exception):
    """Raised when schema validation fails."""

    def __init__(self, message: str, path: str = "", errors: Optional[list[str]] = None):
        self.message = message
        self.path = path
        self.errors = errors or [message]
        super().__init__(message)


def validate_args(args: dict[str, Any], schema: dict[str, Any]) -> list[str]:
    """
    Validate args against a JSON Schema or Python type schema.

    Args:
        args: The arguments dictionary to validate
        schema: JSON Schema or Python type schema (e.g., {"to": str, "body": str})

    Returns:
        List of validation error messages (empty if valid)

    Example:
        >>> errors = validate_args(
        ...     {"to": "user@example.com"},
        ...     {"type": "object", "properties": {"to": {"type": "string"}}, "required": ["to", "body"]}
        ... )
        >>> errors
        ["Missing required field: body"]
    """
    errors: list[str] = []

    # Convert Python type schema to JSON Schema if needed
    json_schema = _normalize_schema(schema)

    # Validate the args
    _validate_object(args, json_schema, "", errors)

    return errors


def _normalize_schema(schema: dict[str, Any]) -> dict[str, Any]:
    """Convert Python type schema to JSON Schema format if needed."""
    if "type" in schema and schema.get("type") == "object":
        # Already in JSON Schema format
        return schema

    # Check if it's a Python type schema (e.g., {"to": str, "body": str})
    if schema and not any(k in schema for k in ["type", "properties", "anyOf", "oneOf"]):
        # Convert Python types to JSON Schema
        properties = {}
        required = []

        for key, type_hint in schema.items():
            prop_schema = _type_to_json_schema(type_hint)
            properties[key] = prop_schema

            # Check if required (not Optional)
            if not _is_optional(type_hint):
                required.append(key)

        return {
            "type": "object",
            "properties": properties,
            "required": required,
        }

    return schema


def _type_to_json_schema(type_hint: Any) -> dict[str, Any]:
    """Convert a Python type hint to JSON Schema."""
    # Handle None/NoneType
    if type_hint is None or type_hint is type(None):
        return {"type": "null"}

    # Handle basic types
    type_mapping = {
        str: {"type": "string"},
        int: {"type": "integer"},
        float: {"type": "number"},
        bool: {"type": "boolean"},
        list: {"type": "array"},
        dict: {"type": "object"},
    }

    if type_hint in type_mapping:
        return type_mapping[type_hint]

    # Handle typing module types
    origin = getattr(type_hint, "__origin__", None)
    args = getattr(type_hint, "__args__", ())

    # Handle Union types (including Optional)
    if origin is Union:
        non_none_args = [a for a in args if a is not type(None)]
        if len(non_none_args) == 1:
            # Optional[X]
            return _type_to_json_schema(non_none_args[0])
        else:
            return {
                "anyOf": [_type_to_json_schema(a) for a in non_none_args]
            }

    # Handle List[X]
    if origin is list:
        if args:
            return {
                "type": "array",
                "items": _type_to_json_schema(args[0])
            }
        return {"type": "array"}

    # Handle Dict[K, V]
    if origin is dict:
        return {"type": "object"}

    # Fallback
    return {"type": "string"}


def _is_optional(type_hint: Any) -> bool:
    """Check if a type hint is Optional."""
    origin = getattr(type_hint, "__origin__", None)
    args = getattr(type_hint, "__args__", ())

    if origin is Union:
        return type(None) in args
    return False


def _validate_object(
    value: Any,
    schema: dict[str, Any],
    path: str,
    errors: list[str],
) -> None:
    """Validate a value against a JSON Schema object."""
    schema_type = schema.get("type")

    # Handle anyOf
    if "anyOf" in schema:
        any_valid = False
        for sub_schema in schema["anyOf"]:
            sub_errors: list[str] = []
            _validate_object(value, sub_schema, path, sub_errors)
            if not sub_errors:
                any_valid = True
                break
        if not any_valid:
            errors.append(f"{_path_str(path)}Value does not match any allowed type")
        return

    # Handle oneOf
    if "oneOf" in schema:
        valid_count = 0
        for sub_schema in schema["oneOf"]:
            sub_errors: list[str] = []
            _validate_object(value, sub_schema, path, sub_errors)
            if not sub_errors:
                valid_count += 1
        if valid_count != 1:
            errors.append(f"{_path_str(path)}Value must match exactly one schema")
        return

    # Type validation
    if schema_type == "object":
        if not isinstance(value, dict):
            errors.append(f"{_path_str(path)}Expected object, got {type(value).__name__}")
            return

        # Required fields
        required = schema.get("required", [])
        for field in required:
            if field not in value:
                errors.append(f"{_path_str(path)}Missing required field: {field}")

        # Validate properties
        properties = schema.get("properties", {})
        for prop_name, prop_value in value.items():
            if prop_name in properties:
                prop_path = f"{path}.{prop_name}" if path else prop_name
                _validate_object(prop_value, properties[prop_name], prop_path, errors)

        # Additional properties check
        additional_properties = schema.get("additionalProperties")
        if additional_properties is False:
            extra_fields = set(value.keys()) - set(properties.keys())
            for field in extra_fields:
                errors.append(f"{_path_str(path)}Unexpected field: {field}")

    elif schema_type == "array":
        if not isinstance(value, list):
            errors.append(f"{_path_str(path)}Expected array, got {type(value).__name__}")
            return

        # Validate items
        items_schema = schema.get("items")
        if items_schema:
            for i, item in enumerate(value):
                item_path = f"{path}[{i}]" if path else f"[{i}]"
                _validate_object(item, items_schema, item_path, errors)

        # Min/max items
        min_items = schema.get("minItems")
        max_items = schema.get("maxItems")
        if min_items is not None and len(value) < min_items:
            errors.append(f"{_path_str(path)}Array must have at least {min_items} items")
        if max_items is not None and len(value) > max_items:
            errors.append(f"{_path_str(path)}Array must have at most {max_items} items")

    elif schema_type == "string":
        if not isinstance(value, str):
            errors.append(f"{_path_str(path)}Expected string, got {type(value).__name__}")
            return

        # Min/max length
        min_length = schema.get("minLength")
        max_length = schema.get("maxLength")
        if min_length is not None and len(value) < min_length:
            errors.append(f"{_path_str(path)}String must be at least {min_length} characters")
        if max_length is not None and len(value) > max_length:
            errors.append(f"{_path_str(path)}String must be at most {max_length} characters")

        # Pattern validation
        pattern = schema.get("pattern")
        if pattern:
            import re
            if not re.match(pattern, value):
                errors.append(f"{_path_str(path)}String does not match pattern: {pattern}")

        # Enum validation
        enum_values = schema.get("enum")
        if enum_values is not None and value not in enum_values:
            errors.append(f"{_path_str(path)}Value must be one of: {enum_values}")

    elif schema_type == "integer":
        if not isinstance(value, int) or isinstance(value, bool):
            errors.append(f"{_path_str(path)}Expected integer, got {type(value).__name__}")
            return
        _validate_number(value, schema, path, errors)

    elif schema_type == "number":
        if not isinstance(value, (int, float)) or isinstance(value, bool):
            errors.append(f"{_path_str(path)}Expected number, got {type(value).__name__}")
            return
        _validate_number(value, schema, path, errors)

    elif schema_type == "boolean":
        if not isinstance(value, bool):
            errors.append(f"{_path_str(path)}Expected boolean, got {type(value).__name__}")

    elif schema_type == "null":
        if value is not None:
            errors.append(f"{_path_str(path)}Expected null, got {type(value).__name__}")


def _validate_number(
    value: Union[int, float],
    schema: dict[str, Any],
    path: str,
    errors: list[str],
) -> None:
    """Validate number constraints."""
    minimum = schema.get("minimum")
    maximum = schema.get("maximum")
    exclusive_minimum = schema.get("exclusiveMinimum")
    exclusive_maximum = schema.get("exclusiveMaximum")

    if minimum is not None and value < minimum:
        errors.append(f"{_path_str(path)}Value must be >= {minimum}")
    if maximum is not None and value > maximum:
        errors.append(f"{_path_str(path)}Value must be <= {maximum}")
    if exclusive_minimum is not None and value <= exclusive_minimum:
        errors.append(f"{_path_str(path)}Value must be > {exclusive_minimum}")
    if exclusive_maximum is not None and value >= exclusive_maximum:
        errors.append(f"{_path_str(path)}Value must be < {exclusive_maximum}")


def _path_str(path: str) -> str:
    """Format path for error message."""
    return f"At '{path}': " if path else ""
