"""Strict input validation for all MCP tool parameters.

Every user-supplied value passes through validation before reaching the API.
Reject early, reject loud.
"""

from __future__ import annotations

import re

_CONTROL_CHARS = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")
_IPV4_RE = re.compile(
    r"^(\d{1,3}\.){3}\d{1,3}(/\d{1,2})?$"
)
_SEVERITIES = frozenset({"info", "low", "medium", "high", "critical"})
_EXPLORE_CATEGORIES = frozenset({
    "products", "services", "titles", "countries",
    "findings", "ports", "technologies", "scans",
})
_SORT_OPTIONS = frozenset({"last_seen", "first_seen", "port", "ip"})

MAX_QUERY_LEN = 500
MAX_STRING_LEN = 200
MAX_SERVICE_ID = 2**31
MAX_PER_PAGE = 200
MAX_SCAN_OUTPUT = 500_000


class ValidationError(Exception):
    """Raised when input validation fails."""


def clean_string(value: str, max_len: int = MAX_STRING_LEN) -> str:
    """Strip control characters, reject null bytes, enforce length."""
    if not isinstance(value, str):
        raise ValidationError(f"Expected string, got {type(value).__name__}")
    if "\x00" in value:
        raise ValidationError("Null byte detected in input")
    cleaned = _CONTROL_CHARS.sub("", value).strip()
    if len(cleaned) > max_len:
        raise ValidationError(f"String exceeds {max_len} character limit")
    return cleaned


def validate_query(raw: str) -> str:
    """Validate a Shodan-style search query string."""
    return clean_string(raw, max_len=MAX_QUERY_LEN)


def validate_ip(raw: str) -> str:
    """Validate an IPv4 or IPv6 address (no CIDR)."""
    cleaned = clean_string(raw, max_len=45)
    if not cleaned:
        raise ValidationError("IP address is required")
    if _IPV4_RE.match(cleaned):
        octets = cleaned.split("/")[0].split(".")
        if all(0 <= int(o) <= 255 for o in octets):
            return cleaned
    if ":" in cleaned:
        return cleaned
    raise ValidationError(f"Invalid IP address: '{raw}'")


def validate_service_id(raw) -> int:
    """Validate service ID as a positive integer."""
    try:
        val = int(raw)
    except (TypeError, ValueError):
        raise ValidationError(f"Service ID must be an integer, got: '{raw}'")
    if val < 1 or val > MAX_SERVICE_ID:
        raise ValidationError(f"Service ID out of range: {val}")
    return val


def validate_limit(raw, *, max_val: int = MAX_PER_PAGE, default: int = 50) -> int:
    """Validate a limit/per_page parameter."""
    if raw is None:
        return default
    try:
        val = int(raw)
    except (TypeError, ValueError):
        raise ValidationError(f"limit must be an integer, got: '{raw}'")
    if val < 1 or val > max_val:
        raise ValidationError(f"limit must be 1-{max_val}, got: {val}")
    return val


def validate_offset(raw) -> int:
    """Validate offset parameter (non-negative integer)."""
    if raw is None:
        return 0
    try:
        val = int(raw)
    except (TypeError, ValueError):
        raise ValidationError(f"offset must be an integer, got: '{raw}'")
    if val < 0:
        raise ValidationError(f"offset must be >= 0, got: {val}")
    return val


def validate_severity(raw: str) -> str:
    """Validate nuclei severity level."""
    cleaned = clean_string(raw).lower()
    if cleaned not in _SEVERITIES:
        raise ValidationError(
            f"Invalid severity: '{raw}'. Must be one of: {', '.join(sorted(_SEVERITIES))}"
        )
    return cleaned


def validate_explore_category(raw: str) -> str:
    """Validate explore page category."""
    cleaned = clean_string(raw).lower()
    if cleaned not in _EXPLORE_CATEGORIES:
        raise ValidationError(
            f"Invalid category: '{raw}'. Must be one of: {', '.join(sorted(_EXPLORE_CATEGORIES))}"
        )
    return cleaned


def validate_sort(raw: str) -> str:
    """Validate search sort order."""
    cleaned = clean_string(raw).lower()
    if cleaned not in _SORT_OPTIONS:
        raise ValidationError(
            f"Invalid sort: '{raw}'. Must be one of: {', '.join(sorted(_SORT_OPTIONS))}"
        )
    return cleaned
