"""Tool wrapping runtime for CleverAgents v3.

Implements the ``wraps`` + ``transform`` + ``argument_mapping`` delegation
mechanism described in the specification (§ Core Concepts > Validation >
Tool Wrapping).

## Architecture

```
WrappedToolExecutor
  |-- ArgumentMapper    (translate wrapper args -> wrapped tool args)
  |-- TransformExecutor (sandboxed transform function on tool output)
  +-- ToolRuntime       (resolve + invoke wrapped tools)
```

## Delegation Chain

A validation wrapping another validation forms a delegation chain:

```
validation-A  ->  validation-B  ->  tool-C
(wraps B)         (wraps C)         (leaf)
```

``WrappedToolExecutor.execute()`` recursively resolves the chain,
detecting cycles and enforcing a maximum depth.

## Sandbox

``TransformExecutor`` compiles and executes the user-supplied
``transform`` function with a restricted set of built-in names.
No filesystem, network, or import access is available inside the
transform.

Based on ``docs/specification.md`` § Tool Wrapping.
"""

from __future__ import annotations

import logging
from typing import Any

from cleveragents.domain.models.core.tool import Validation

logger = logging.getLogger(__name__)

# Maximum delegation chain depth to prevent infinite recursion.
MAX_WRAPPING_DEPTH = 10

# Safe built-ins available inside the ``transform`` sandbox.
_SAFE_BUILTINS: dict[str, Any] = {
    "True": True,
    "False": False,
    "None": None,
    "abs": abs,
    "all": all,
    "any": any,
    "bool": bool,
    "dict": dict,
    "enumerate": enumerate,
    "float": float,
    "frozenset": frozenset,
    "int": int,
    "isinstance": isinstance,
    "len": len,
    "list": list,
    "map": map,
    "max": max,
    "min": min,
    "range": range,
    "repr": repr,
    "reversed": reversed,
    "round": round,
    "set": set,
    "sorted": sorted,
    "str": str,
    "sum": sum,
    "tuple": tuple,
    "type": type,
    "zip": zip,
}


# -------------------------------------------------------------------
# Errors
# -------------------------------------------------------------------


class WrappedToolNotFoundError(Exception):
    """Raised when the tool referenced by ``wraps`` is not registered."""

    def __init__(self, tool_name: str, wraps_name: str) -> None:
        self.tool_name = tool_name
        self.wraps_name = wraps_name
        super().__init__(
            f"Validation '{tool_name}' wraps '{wraps_name}' but "
            f"'{wraps_name}' is not registered in the runtime"
        )


class WrappingCycleError(Exception):
    """Raised when a circular wrapping chain is detected."""

    def __init__(self, chain: list[str]) -> None:
        self.chain = list(chain)
        cycle_str = " -> ".join(chain)
        super().__init__(f"Circular wrapping chain detected: {cycle_str}")


class WrappingDepthExceededError(Exception):
    """Raised when the delegation chain exceeds ``MAX_WRAPPING_DEPTH``."""

    def __init__(self, depth: int) -> None:
        self.depth = depth
        super().__init__(
            f"Wrapping delegation chain depth ({depth}) exceeds "
            f"the maximum ({MAX_WRAPPING_DEPTH})"
        )


class TransformExecutionError(Exception):
    """Raised when the ``transform`` function fails during execution."""

    def __init__(self, tool_name: str, detail: str) -> None:
        self.tool_name = tool_name
        self.detail = detail
        super().__init__(f"Transform function for '{tool_name}' failed: {detail}")


# -------------------------------------------------------------------
# ArgumentMapper
# -------------------------------------------------------------------


class ArgumentMapper:
    """Applies ``argument_mapping`` to translate arguments.

    When a validation wraps a tool, the validation may accept different
    input arguments.  The ``argument_mapping`` dictionary specifies how
    the validation's inputs map to the wrapped tool's expected inputs.

    Keys are the wrapped tool's parameter names. Values are either:
    - A string referencing a validation input parameter (forwarded).
    - A literal value (string, number, boolean) always sent to the
      wrapped tool.

    When ``argument_mapping`` is ``None``, input arguments are passed
    through to the wrapped tool unchanged (identity mapping).
    """

    def __init__(self, mapping: dict[str, Any] | None) -> None:
        if mapping is not None and not isinstance(mapping, dict):
            raise TypeError(
                f"argument_mapping must be a dict or None, got {type(mapping).__name__}"
            )
        self._mapping = mapping

    @property
    def mapping(self) -> dict[str, Any] | None:
        """Return the raw mapping configuration."""
        return self._mapping

    def apply(self, validation_inputs: dict[str, Any]) -> dict[str, Any]:
        """Translate *validation_inputs* into wrapped-tool arguments.

        Parameters
        ----------
        validation_inputs:
            The arguments the validation was invoked with.

        Returns
        -------
        dict[str, Any]:
            Arguments to forward to the wrapped tool.
        """
        if not isinstance(validation_inputs, dict):
            raise TypeError(
                f"validation_inputs must be a dict, "
                f"got {type(validation_inputs).__name__}"
            )
        if self._mapping is None:
            return dict(validation_inputs)

        mapped: dict[str, Any] = {}
        for wrapped_param, source in self._mapping.items():
            if isinstance(source, str) and source in validation_inputs:
                mapped[wrapped_param] = validation_inputs[source]
            else:
                # Literal value (or string not present as a key -- use
                # as literal).
                mapped[wrapped_param] = source
        return mapped


# -------------------------------------------------------------------
# TransformExecutor
# -------------------------------------------------------------------


class TransformExecutor:
    """Executes the ``transform`` function in a sandboxed context.

    The transform code must define a function called ``transform`` that
    accepts one positional argument (the wrapped tool's output) and
    returns a dict with at minimum ``{"passed": bool}``.

    The sandbox disallows imports, file I/O, and network access by
    restricting available built-ins.
    """

    def __init__(self, transform_code: str, tool_name: str) -> None:
        if not isinstance(transform_code, str):
            raise TypeError(
                f"transform_code must be a str, got {type(transform_code).__name__}"
            )
        if not transform_code.strip():
            raise ValueError("transform_code must not be empty")
        if not isinstance(tool_name, str):
            raise TypeError(f"tool_name must be a str, got {type(tool_name).__name__}")
        self._code = transform_code
        self._tool_name = tool_name
        self._compiled = compile(  # nosemgrep: no-compile-exec
            self._code,
            f"<transform:{tool_name}>",
            "exec",
        )

    def execute(self, tool_output: Any) -> dict[str, Any]:
        """Run the transform on *tool_output*.

        Returns
        -------
        dict[str, Any]:
            The validation-format result (must contain ``passed``).

        Raises
        ------
        TransformExecutionError:
            If the transform function is missing, raises, or returns
            an invalid result.
        """
        sandbox: dict[str, Any] = {"__builtins__": dict(_SAFE_BUILTINS)}

        try:
            exec(  # nosec B102 # nosemgrep: no-exec
                self._compiled,
                sandbox,
            )
        except Exception as exc:
            raise TransformExecutionError(
                self._tool_name,
                f"Failed to compile/execute transform code: {exc}",
            ) from exc

        transform_fn = sandbox.get("transform")
        if transform_fn is None or not callable(transform_fn):
            raise TransformExecutionError(
                self._tool_name,
                "Transform code must define a callable 'transform' function",
            )

        try:
            result = transform_fn(tool_output)
        except Exception as exc:
            raise TransformExecutionError(
                self._tool_name,
                f"Transform function raised: {type(exc).__name__}: {exc}",
            ) from exc

        if not isinstance(result, dict):
            raise TransformExecutionError(
                self._tool_name,
                f"Transform must return a dict, got {type(result).__name__}",
            )

        if "passed" not in result:
            raise TransformExecutionError(
                self._tool_name,
                "Transform result must contain a 'passed' key",
            )

        return result


# -------------------------------------------------------------------
# WrappedToolExecutor
# -------------------------------------------------------------------


class WrappedToolExecutor:
    """Resolves ``wraps`` references and delegates execution.

    Orchestrates the full wrapping flow:

    1. Resolve the wrapped tool from the tool lookup.
    2. If the wrapped tool is itself a wrapping validation, recurse.
    3. Apply ``argument_mapping`` to translate arguments.
    4. Execute the leaf tool.
    5. Walk back up the chain applying ``transform`` at each level.

    Parameters
    ----------
    tool_lookup:
        A callable that accepts a tool name (str) and returns
        the ``Validation`` or ``Tool`` domain model, or ``None``
        if not found.
    tool_executor:
        A callable ``(tool_name, mapped_args) -> dict`` that
        actually runs the leaf tool and returns its raw output.
    """

    def __init__(
        self,
        tool_lookup: Any,
        tool_executor: Any,
    ) -> None:
        if tool_lookup is None:
            raise ValueError("tool_lookup must not be None")
        if tool_executor is None:
            raise ValueError("tool_executor must not be None")
        if not callable(tool_lookup):
            raise TypeError(
                f"tool_lookup must be callable, got {type(tool_lookup).__name__}"
            )
        if not callable(tool_executor):
            raise TypeError(
                f"tool_executor must be callable, got {type(tool_executor).__name__}"
            )
        self._tool_lookup = tool_lookup
        self._tool_executor = tool_executor

    def execute(
        self,
        validation: Validation,
        inputs: dict[str, Any],
    ) -> dict[str, Any]:
        """Execute a wrapping validation.

        Parameters
        ----------
        validation:
            The wrapping ``Validation`` (must have ``wraps`` set).
        inputs:
            The arguments the validation was invoked with.

        Returns
        -------
        dict[str, Any]:
            The validation-format result with ``passed``, optionally
            ``message`` and ``data``.

        Raises
        ------
        WrappedToolNotFoundError:
            If any tool in the chain is not registered.
        WrappingCycleError:
            If the chain contains a cycle.
        WrappingDepthExceededError:
            If the chain exceeds ``MAX_WRAPPING_DEPTH``.
        TransformExecutionError:
            If any transform function in the chain fails.
        """
        if not isinstance(validation, Validation):
            raise TypeError(
                f"validation must be a Validation, got {type(validation).__name__}"
            )
        if not isinstance(inputs, dict):
            raise TypeError(f"inputs must be a dict, got {type(inputs).__name__}")
        if validation.wraps is None:
            raise ValueError(
                f"Validation '{validation.name}' does not have "
                f"'wraps' set -- use direct execution instead"
            )

        chain = self._resolve_chain(validation)
        return self._execute_chain(chain, inputs)

    # -- Chain resolution --------------------------------------------------

    def _resolve_chain(
        self,
        validation: Validation,
    ) -> list[Validation]:
        """Walk the wraps chain and return the ordered list.

        The first element is the outermost wrapper, the last is the
        innermost wrapper (whose ``wraps`` target is the leaf tool).
        """
        chain: list[Validation] = []
        visited: set[str] = set()
        current: Validation = validation

        while True:
            if len(chain) >= MAX_WRAPPING_DEPTH:
                raise WrappingDepthExceededError(len(chain))

            if current.name in visited:
                cycle = [v.name for v in chain] + [current.name]
                raise WrappingCycleError(cycle)

            visited.add(current.name)
            chain.append(current)

            wraps_name = current.wraps
            if wraps_name is None:
                break

            wrapped = self._tool_lookup(wraps_name)
            if wrapped is None:
                raise WrappedToolNotFoundError(current.name, wraps_name)

            if isinstance(wrapped, Validation) and wrapped.wraps is not None:
                current = wrapped
            else:
                # Leaf tool found -- stop the chain
                break

        return chain

    # -- Chain execution ---------------------------------------------------

    def _execute_chain(
        self,
        chain: list[Validation],
        inputs: dict[str, Any],
    ) -> dict[str, Any]:
        """Execute the delegation chain.

        1. Walk from outermost to innermost, applying argument mappings.
        2. Execute the leaf tool.
        3. Walk back from innermost to outermost, applying transforms.
        """
        # Build the fully-mapped arguments by applying each wrapper's
        # argument_mapping in sequence.
        mapped_args = dict(inputs)
        for wrapper in chain:
            mapper = ArgumentMapper(wrapper.argument_mapping)
            mapped_args = mapper.apply(mapped_args)

        # Execute the leaf tool (the target of the innermost wraps)
        leaf_wrapper = chain[-1]
        leaf_tool_name = leaf_wrapper.wraps
        if leaf_tool_name is None:
            raise ValueError(
                f"Innermost wrapper '{leaf_wrapper.name}' has no 'wraps' target"
            )

        logger.info(
            "Executing wrapped tool",
            extra={
                "chain": [v.name for v in chain],
                "leaf_tool": leaf_tool_name,
            },
        )

        raw_output = self._tool_executor(leaf_tool_name, mapped_args)

        # Apply transforms in reverse (innermost first, outermost last)
        result: dict[str, Any] = (
            raw_output if isinstance(raw_output, dict) else {"raw": raw_output}
        )
        for wrapper in reversed(chain):
            if wrapper.transform is not None:
                transform_exec = TransformExecutor(
                    wrapper.transform,
                    wrapper.name,
                )
                logger.debug(
                    "Applying transform",
                    extra={
                        "wrapper": wrapper.name,
                        "input_type": type(result).__name__,
                    },
                )
                result = transform_exec.execute(result)

        return result
