"""Shared validation runner — start a server and run all validation rules.

Extracted from ``validate_cmd`` so that ``eval``, ``dev``, and other
entry points can reuse validation logic without importing CLI code.
"""

from __future__ import annotations

from datetime import datetime
from typing import Any

from hatchdx.harness.simulator import SimulatorError, StdioSimulator, ToolCallResult, ToolInfo
from hatchdx.validator.models import ValidatorError, ValidationReport
from hatchdx.validator.registry import get_rules

# Ensure rule decorators are executed.
import hatchdx.validator.rules  # noqa: F401

# Type-to-invalid-value mapping for provoking errors on optional-only tools.
_INVALID_VALUES: dict[str, Any] = {
    "string": 99999,
    "number": "not_a_number",
    "integer": "not_an_integer",
    "boolean": "not_a_boolean",
    "array": "not_an_array",
    "object": "not_an_object",
}


def _build_bad_input(tool: ToolInfo) -> dict[str, Any]:
    """Construct deliberately bad input to provoke an error from a tool.

    Strategy:
    1. If the schema has required fields, omit them (send {}) — should
       trigger a "missing required field" error.
    2. If all fields are optional, send an invalid type for one property.
    3. If the schema has no properties at all, fall back to {}.
    """
    schema = tool.input_schema
    if not schema or not isinstance(schema, dict):
        return {}

    required = schema.get("required", [])
    if required:
        # Omitting required fields should provoke an error.
        return {}

    properties = schema.get("properties", {})
    if not properties:
        return {}

    # All fields are optional — send an invalid type for the first property.
    for prop_name, prop_schema in properties.items():
        prop_type = prop_schema.get("type", "string") if isinstance(prop_schema, dict) else "string"
        invalid = _INVALID_VALUES.get(prop_type, "INVALID")
        return {prop_name: invalid}

    return {}


async def run_validation(
    command: list[str],
    server_name: str,
    timeout: float,
    category_filter: str | None = None,
    severity_filter: str | None = None,
) -> ValidationReport:
    """Start the server, discover tools, and run all validation rules.

    This is the core validation logic shared across ``validate``, ``eval``,
    and ``dev`` commands.
    """
    async with StdioSimulator(command, timeout=timeout) as sim:
        return await run_validation_with_simulator(
            sim,
            server_name=server_name,
            category_filter=category_filter,
            severity_filter=severity_filter,
        )


async def run_validation_with_simulator(
    sim: StdioSimulator,
    *,
    server_name: str,
    category_filter: str | None = None,
    severity_filter: str | None = None,
) -> ValidationReport:
    """Run validation against an already-started simulator.

    Useful when a simulator is already running (e.g. inside ``dev`` or
    ``eval``), so we avoid spawning a second server process.
    """
    tools = await sim.list_tools()

    if not tools:
        raise ValidatorError(
            "Server returned no tools. Nothing to validate.\n"
            "Make sure your server registers at least one tool."
        )

    # Collect error results by calling each tool with bad input.
    error_results: dict[str, ToolCallResult] = {}
    call_results: dict[str, ToolCallResult] = {}

    for tool in tools:
        try:
            bad_input = _build_bad_input(tool)
            result = await sim.call_tool(tool.name, bad_input)
            if result.is_error:
                error_results[tool.name] = result
            else:
                call_results[tool.name] = result
        except SimulatorError:
            pass

    # Get rules, optionally filtered.
    rules = get_rules(category=category_filter, severity=severity_filter)

    # Run each rule.
    all_results = []
    for rule in rules:
        try:
            results = rule.check(
                tools,
                server_name=server_name,
                error_results=error_results if error_results else None,
                call_results=call_results if call_results else None,
            )
            all_results.extend(results)
        except Exception as exc:
            raise ValidatorError(
                f"Rule '{rule.name}' failed with error: {exc}"
            ) from exc

    return ValidationReport(
        results=all_results,
        server_name=server_name,
        tools_checked=len(tools),
        timestamp=datetime.now(),
    )
