"""Task directory validator — pre-loop gate.

Called by: eval.py, optimize.py (before any trials run)

Validates task contracts and runnable evaluate API before optimization
starts. If validation fails, optimization must not begin.

Scope: task directory structure + evaluate.py callable contract.
NOT in scope: optimizer-loop behavioral checks, oracle solvability.
"""

from __future__ import annotations

import importlib.util
import inspect
import logging
import sys
import tomllib
from pathlib import Path

logger = logging.getLogger(__name__)


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

    Contains a list of human-readable error strings.
    """

    def __init__(self, errors: list[str]):
        self.errors = errors
        super().__init__(
            "Task validation failed:\n" + "\n".join(f"  - {e}" for e in errors)
        )


def validate_task_dir(task_dir: Path) -> list[str]:
    """Validate a single task directory contract.

    Returns list of error strings (empty if valid).
    """
    errors: list[str] = []
    task_dir = Path(task_dir)

    if not task_dir.is_dir():
        return [f"{task_dir}: not a directory"]

    # Required files
    if not (task_dir / "task.toml").exists():
        errors.append(f"{task_dir.name}: missing task.toml")

    if not (task_dir / "instruction.md").exists():
        errors.append(f"{task_dir.name}: missing instruction.md")

    evaluate_path = task_dir / "tests" / "evaluate.py"
    if not evaluate_path.exists():
        errors.append(f"{task_dir.name}: missing tests/evaluate.py")

    return errors


def validate_evaluate_callable(task_dir: Path) -> list[str]:
    """Validate the evaluate.py runnable API contract.

    Checks that tests/evaluate.py can be imported and exposes an
    evaluate callable with a compatible signature.

    Returns list of error strings (empty if valid).
    """
    errors: list[str] = []
    task_dir = Path(task_dir)
    evaluate_path = task_dir / "tests" / "evaluate.py"

    if not evaluate_path.exists():
        return []  # Already caught by validate_task_dir

    module_name = f"_clawbench_validate_{task_dir.name}"
    spec = importlib.util.spec_from_file_location(module_name, evaluate_path)
    if spec is None or spec.loader is None:
        errors.append(f"{task_dir.name}: cannot create module spec for tests/evaluate.py")
        return errors

    try:
        module = importlib.util.module_from_spec(spec)
        # Don't pollute sys.modules permanently
        old_module = sys.modules.get(module_name)
        sys.modules[module_name] = module
        try:
            spec.loader.exec_module(module)
        finally:
            if old_module is None:
                sys.modules.pop(module_name, None)
            else:
                sys.modules[module_name] = old_module
    except Exception as exc:
        errors.append(
            f"{task_dir.name}: tests/evaluate.py import failed: {type(exc).__name__}: {exc}"
        )
        return errors

    # Check for evaluate callable
    evaluate_fn = getattr(module, "evaluate", None)
    if evaluate_fn is None:
        errors.append(f"{task_dir.name}: tests/evaluate.py has no 'evaluate' callable")
        return errors

    if not callable(evaluate_fn):
        errors.append(f"{task_dir.name}: tests/evaluate.py 'evaluate' is not callable")
        return errors

    # Check signature — must accept at least one positional arg
    try:
        sig = inspect.signature(evaluate_fn)
        params = list(sig.parameters.values())
        if not params:
            errors.append(
                f"{task_dir.name}: tests/evaluate.py evaluate() takes no arguments "
                "(expected at least 1)"
            )
    except (ValueError, TypeError):
        # Can't inspect signature — allow it through
        pass

    return errors


def validate_tasks(
    task_paths: list[dict],
    check_callable: bool = True,
) -> None:
    """Validate all task directories before optimization.

    Args:
        task_paths: List of {"task_name": ..., "task_path": ...} dicts.
        check_callable: Whether to check evaluate.py callable contract.

    Raises:
        ValidationError: If any task fails validation.
    """
    all_errors: list[str] = []

    if not task_paths:
        raise ValidationError(["No tasks provided for validation. Run 'clawbench check --tasks-dir <path>' to debug."])

    # Check for duplicate task names
    names = [t["task_name"] for t in task_paths]
    seen: dict[str, int] = {}
    for name in names:
        seen[name] = seen.get(name, 0) + 1
    duplicates = {n: c for n, c in seen.items() if c > 1}
    if duplicates:
        for name, count in duplicates.items():
            logger.warning(f"Duplicate task name '{name}' appears {count} times")

    # Validate each task directory
    for task in task_paths:
        task_dir = Path(task["task_path"])

        # Static contract checks
        errors = validate_task_dir(task_dir)
        all_errors.extend(errors)

        # Runnable API contract checks
        if check_callable and not errors:
            api_errors = validate_evaluate_callable(task_dir)
            all_errors.extend(api_errors)

    if all_errors:
        raise ValidationError(all_errors)

    logger.info(f"Validated {len(task_paths)} task(s) — all checks passed")
