"""
Validator utilities for the mixed C++/Python pipeline backend and its plugins.

This follows the reference design in PIPELINE_SPEC.md:
1) Validate backend metadata and runtime API surface.
2) Validate plugin metadata against schema and backend versions/ABI.
3) Validate at runtime that declared operations are actually registered
   and that Python ops expose a single-argument callable.
"""

from __future__ import annotations

import importlib
import inspect
import json
from pathlib import Path
from typing import Any, Dict

import jsonschema


def validate_backend(module_name: str, backend_schema: Dict[str, Any]) -> Dict[str, Any]:
    """
    Validate backend metadata and runtime surface.
    Returns the backend metadata dict on success.
    """
    backend = importlib.import_module(module_name)

    if not hasattr(backend, "__backend_metadata__"):
        raise RuntimeError(f"Backend {module_name} has no __backend_metadata__")

    meta = backend.__backend_metadata__
    jsonschema.validate(meta, backend_schema)

    if not hasattr(backend, "list_ops"):
        raise RuntimeError("Backend must implement list_ops()")

    if not hasattr(backend, "describe_ops"):
        raise RuntimeError("Backend must implement describe_ops()")

    ops = backend.list_ops()
    if not isinstance(ops, (list, tuple)):
        raise RuntimeError("backend.list_ops() must return a list or tuple")

    desc = backend.describe_ops()
    if not isinstance(desc, (list, tuple)):
        raise RuntimeError("backend.describe_ops() must return a list of dicts")

    for entry in desc:
        if not isinstance(entry, dict):
            raise RuntimeError("describe_ops() entries must be dicts")
        if "name" not in entry or "impl" not in entry:
            raise RuntimeError("describe_ops() entries must contain 'name' and 'impl'")

    return meta


def load_plugin_metadata(package_root: Path) -> Dict[str, Any]:
    """
    Load pipeline_plugin.json from a plugin package root.
    """
    meta_path = package_root / "pipeline_plugin.json"
    if not meta_path.exists():
        raise FileNotFoundError(f"No pipeline_plugin.json found in {package_root}")
    return json.loads(meta_path.read_text(encoding="utf-8"))


def validate_plugin_metadata(
    plugin_meta: Dict[str, Any],
    plugin_schema: Dict[str, Any],
    backend_meta: Dict[str, Any],
) -> None:
    """
    Validate plugin metadata against schema and backend API/ABI expectations.
    """
    jsonschema.validate(plugin_meta, plugin_schema)

    if plugin_meta["backend_api_version"] != backend_meta["api_version"]:
        raise RuntimeError(
            f"Plugin backend_api_version={plugin_meta['backend_api_version']} "
            f"does not match backend api_version={backend_meta['api_version']}"
        )

    if plugin_meta["backend_data_abi"] != backend_meta["data_abi_tag"]:
        raise RuntimeError(
            f"Plugin backend_data_abi={plugin_meta['backend_data_abi']} "
            f"does not match backend data_abi_tag={backend_meta['data_abi_tag']}"
        )


def validate_plugin_runtime(
    backend_module_name: str,
    plugin_module_name: str,
    plugin_meta: Dict[str, Any],
) -> None:
    """
    Ensure declared operations are registered in the backend with matching impl.
    Also checks Python ops expose a single-argument callable.
    """
    backend = importlib.import_module(backend_module_name)

    # Import plugin – triggers registration of C++ and/or Python ops.
    importlib.import_module(plugin_module_name)

    desc = backend.describe_ops()
    desc_by_name = {e["name"]: e for e in desc}

    for op in plugin_meta["operations"]:
        name = op["name"]
        declared_type = op["type"]

        if name not in desc_by_name:
            raise RuntimeError(
                f"Plugin {plugin_module_name} declares op '{name}' "
                f"but backend does not list it after import."
            )

        impl = desc_by_name[name].get("impl")
        if impl != declared_type:
            raise RuntimeError(
                f"Plugin {plugin_module_name} declares op '{name}' as type '{declared_type}', "
                f"but backend reports impl='{impl}'."
            )

        if declared_type == "python":
            py_obj = desc_by_name[name].get("callable")
            if py_obj is None or not callable(py_obj):
                raise RuntimeError(
                    f"Python op '{name}' has no callable associated in backend.describe_ops()."
                )

            sig = inspect.signature(py_obj)
            if len(sig.parameters) != 1:
                raise RuntimeError(
                    f"Python op '{name}' must accept exactly one parameter (Data); "
                    f"got parameters {list(sig.parameters.keys())}"
                )


def validate_plugin(
    backend_module_name: str,
    plugin_module_name: str,
    backend_schema: Dict[str, Any],
    plugin_schema: Dict[str, Any],
    package_root: Path,
) -> None:
    """
    Full end-to-end validation helper combining static and runtime checks.
    """
    backend_meta = validate_backend(backend_module_name, backend_schema)

    plugin_meta = load_plugin_metadata(package_root)
    validate_plugin_metadata(plugin_meta, plugin_schema, backend_meta)

    validate_plugin_runtime(
        backend_module_name=backend_module_name,
        plugin_module_name=plugin_module_name,
        plugin_meta=plugin_meta,
    )
