#!/usr/bin/env python3
"""
Input Validation Layer (UVS-H06)
================================
Validates all tool arguments before execution.

Security Features:
- Coordinate bounds validation (0-3840 x 0-2160)
- String length limits (max 1000 chars)
- Enum value validation
- Type checking

VERIFICATION_STAMP
Story: UVS-H06
Verified By: Claude Opus 4.5
Verified At: 2026-02-03
Tests: See verification/test_input_validator.py
"""

import logging
from typing import Any, List, Optional, Dict, Union
from dataclasses import dataclass
from enum import Enum

logger = logging.getLogger(__name__)


class ValidationError(Exception):
    """Raised when input validation fails."""
    pass


@dataclass
class ValidationResult:
    """Result of a validation check."""
    valid: bool
    value: Any
    error: Optional[str] = None


# Screen bounds (4K max)
MAX_VIEWPORT_WIDTH = 3840
MAX_VIEWPORT_HEIGHT = 2160
MIN_COORDINATE = 0

# String limits
MAX_STRING_LENGTH = 1000
MAX_SELECTOR_LENGTH = 500
MAX_URL_LENGTH = 2048


def validate_coordinates(
    x: Any,
    y: Any,
    max_width: int = MAX_VIEWPORT_WIDTH,
    max_height: int = MAX_VIEWPORT_HEIGHT
) -> ValidationResult:
    """
    Validate screen coordinates are within bounds.

    Args:
        x: X coordinate
        y: Y coordinate
        max_width: Maximum X value (default 3840)
        max_height: Maximum Y value (default 2160)

    Returns:
        ValidationResult with clamped coordinates if out of bounds

    Raises:
        ValidationError: If coordinates are invalid type
    """
    try:
        x_int = int(x)
        y_int = int(y)
    except (TypeError, ValueError) as e:
        logger.warning(f"[VALIDATION] Invalid coordinate types: x={x!r}, y={y!r}")
        raise ValidationError(f"Coordinates must be numeric: {e}")

    errors = []

    # Clamp to bounds
    if x_int < MIN_COORDINATE:
        errors.append(f"x={x_int} below minimum {MIN_COORDINATE}")
        x_int = MIN_COORDINATE
    elif x_int > max_width:
        errors.append(f"x={x_int} above maximum {max_width}")
        x_int = max_width

    if y_int < MIN_COORDINATE:
        errors.append(f"y={y_int} below minimum {MIN_COORDINATE}")
        y_int = MIN_COORDINATE
    elif y_int > max_height:
        errors.append(f"y={y_int} above maximum {max_height}")
        y_int = max_height

    if errors:
        logger.warning(f"[VALIDATION] Coordinates clamped: {errors}")

    return ValidationResult(
        valid=len(errors) == 0,
        value=(x_int, y_int),
        error="; ".join(errors) if errors else None
    )


def validate_string_length(
    value: Any,
    max_length: int = MAX_STRING_LENGTH,
    field_name: str = "value"
) -> ValidationResult:
    """
    Validate string length is within bounds.

    Args:
        value: String to validate
        max_length: Maximum allowed length
        field_name: Name of field for error messages

    Returns:
        ValidationResult with truncated string if too long

    Raises:
        ValidationError: If value is not a string
    """
    if value is None:
        return ValidationResult(valid=True, value="")

    if not isinstance(value, str):
        try:
            value = str(value)
        except Exception as e:
            raise ValidationError(f"{field_name} must be a string: {e}")

    if len(value) > max_length:
        logger.warning(f"[VALIDATION] {field_name} truncated: {len(value)} -> {max_length}")
        return ValidationResult(
            valid=False,
            value=value[:max_length],
            error=f"{field_name} exceeds max length {max_length}"
        )

    return ValidationResult(valid=True, value=value)


def validate_enum(
    value: Any,
    allowed_values: List[Any],
    field_name: str = "value",
    case_insensitive: bool = False
) -> ValidationResult:
    """
    Validate value is in allowed set.

    Args:
        value: Value to validate
        allowed_values: List of allowed values
        field_name: Name of field for error messages
        case_insensitive: If True, compare strings case-insensitively

    Returns:
        ValidationResult

    Raises:
        ValidationError: If value not in allowed set
    """
    check_value = value
    check_allowed = allowed_values

    if case_insensitive and isinstance(value, str):
        check_value = value.lower()
        check_allowed = [v.lower() if isinstance(v, str) else v for v in allowed_values]

    if check_value not in check_allowed:
        logger.warning(f"[VALIDATION] Invalid {field_name}: {value!r} not in {allowed_values}")
        raise ValidationError(f"{field_name} must be one of: {allowed_values}")

    return ValidationResult(valid=True, value=value)


def validate_url(url: Any, allowed_schemes: List[str] = None) -> ValidationResult:
    """
    Validate URL format and scheme.

    Args:
        url: URL to validate
        allowed_schemes: List of allowed schemes (default: ['https', 'http'])

    Returns:
        ValidationResult

    Raises:
        ValidationError: If URL is invalid or scheme not allowed
    """
    from urllib.parse import urlparse

    if allowed_schemes is None:
        allowed_schemes = ['https', 'http']

    if not isinstance(url, str):
        raise ValidationError("URL must be a string")

    # Length check
    if len(url) > MAX_URL_LENGTH:
        raise ValidationError(f"URL exceeds max length {MAX_URL_LENGTH}")

    try:
        parsed = urlparse(url)
    except Exception as e:
        raise ValidationError(f"Invalid URL format: {e}")

    if not parsed.scheme:
        raise ValidationError("URL must include scheme (e.g., https://)")

    if parsed.scheme.lower() not in [s.lower() for s in allowed_schemes]:
        logger.warning(f"[VALIDATION] Blocked URL scheme: {parsed.scheme}")
        raise ValidationError(f"URL scheme must be one of: {allowed_schemes}")

    if not parsed.netloc:
        raise ValidationError("URL must include host")

    return ValidationResult(valid=True, value=url)


def validate_tool_args(
    args: Dict[str, Any],
    schema: Dict[str, Dict]
) -> Dict[str, Any]:
    """
    Validate all tool arguments against a schema.

    Args:
        args: Dictionary of argument name -> value
        schema: Dictionary of argument name -> validation config
            {
                "x": {"type": "coordinate", "max": 1920},
                "selector": {"type": "string", "max_length": 500},
                "action": {"type": "enum", "values": ["click", "type"]}
            }

    Returns:
        Dictionary of validated/sanitized arguments

    Raises:
        ValidationError: If any validation fails
    """
    validated = {}

    for name, config in schema.items():
        value = args.get(name)
        val_type = config.get("type", "string")

        # Check required
        if config.get("required", False) and value is None:
            raise ValidationError(f"Missing required argument: {name}")

        if value is None:
            validated[name] = config.get("default")
            continue

        # Type-specific validation
        if val_type == "coordinate":
            result = validate_coordinates(
                value,
                args.get(config.get("y_field", "y")),
                config.get("max_x", MAX_VIEWPORT_WIDTH),
                config.get("max_y", MAX_VIEWPORT_HEIGHT)
            )
            validated[name] = result.value[0]
            if config.get("y_field"):
                validated[config["y_field"]] = result.value[1]

        elif val_type == "string":
            result = validate_string_length(
                value,
                config.get("max_length", MAX_STRING_LENGTH),
                name
            )
            validated[name] = result.value

        elif val_type == "enum":
            result = validate_enum(
                value,
                config.get("values", []),
                name,
                config.get("case_insensitive", False)
            )
            validated[name] = result.value

        elif val_type == "url":
            result = validate_url(
                value,
                config.get("allowed_schemes")
            )
            validated[name] = result.value

        elif val_type == "int":
            try:
                int_val = int(value)
                if "min" in config and int_val < config["min"]:
                    int_val = config["min"]
                if "max" in config and int_val > config["max"]:
                    int_val = config["max"]
                validated[name] = int_val
            except (TypeError, ValueError):
                raise ValidationError(f"{name} must be an integer")

        elif val_type == "bool":
            if isinstance(value, bool):
                validated[name] = value
            elif isinstance(value, str):
                validated[name] = value.lower() in ('true', '1', 'yes')
            else:
                validated[name] = bool(value)

        else:
            # Unknown type, pass through
            validated[name] = value

    return validated


# Pre-defined schemas for common tool types
CURSOR_TOOL_SCHEMA = {
    "x": {"type": "int", "required": True, "min": 0, "max": MAX_VIEWPORT_WIDTH},
    "y": {"type": "int", "required": True, "min": 0, "max": MAX_VIEWPORT_HEIGHT}
}

SELECTOR_TOOL_SCHEMA = {
    "selector": {"type": "string", "required": True, "max_length": MAX_SELECTOR_LENGTH}
}

GESTURE_TOOL_SCHEMA = {
    "type": {"type": "enum", "required": True, "values": ["circle", "underline", "point"]},
    "selector": {"type": "string", "required": True, "max_length": MAX_SELECTOR_LENGTH}
}

ZOOM_TOOL_SCHEMA = {
    "x": {"type": "int", "required": True, "min": 0, "max": MAX_VIEWPORT_WIDTH},
    "y": {"type": "int", "required": True, "min": 0, "max": MAX_VIEWPORT_HEIGHT},
    "width": {"type": "int", "required": False, "min": 100, "max": 1920, "default": 400},
    "height": {"type": "int", "required": False, "min": 100, "max": 1080, "default": 400}
}
