"""CTL tools factory for agent integration.

Creates ctl_* lifecycle tools for registration with PydanticAI agents.
"""

# @invar:allow shell_result: Tool factory returns list of Tool objects (not I/O result)
# @shell_orchestration: Tool factory helpers orchestrate tool creation
# @invar:allow function_size: Multiple tool definitions in single factory function
# @shell_complexity: Tool wrapping requires multiple async context handlers

from inspect import Parameter, Signature, signature
from collections.abc import Callable
from typing import TYPE_CHECKING, TypedDict, cast

from returns.result import Failure

from anima.shell.state import read_state


if TYPE_CHECKING:
    from anima.shell.agent import AgentRunner


class ApprovalToolPayload(TypedDict):
    """Approval decision payload returned by ctl_request_approval.

    Schema is aligned with shell state-channel payload:
    {"approved": bool, "note": str | None}.
    """

    approved: bool
    note: str | None


# @invar:allow shell_result: Tool factory returns list of Tool objects (not I/O result)
# @shell_complexity: Tool wrapping requires async handlers and imports
# @invar:allow function_size: Tool factory with multiple tool definitions
# @shell_orchestration: Tool factory with multiple tool definitions
def create_ctl_tools(
    instance_id: str,
    agent_runner: "AgentRunner",
) -> list:
    """Create ctl_* lifecycle tools for agent integration.

    Returns a list of tool functions for registration with PydanticAI agent.
    Note: External tool registration may use different mechanisms.
    This preserves the interface while respecting override detection.

    Args:
        instance_id: The agent's unique identifier.
        agent_runner: Reference to the AgentRunner instance.

    Returns:
        List of Tool objects (ctl_finish, ctl_request_approval,
        ctl_spawn, ctl_delegate, ctl_await, ctl_poll, ctl_cancel,
        ctl_child_logs, ctl_switch_persona).
    """

    def _unexpected_kwargs(kwargs: dict[str, object]) -> dict[str, object]:
        """Filter known schema noise while rejecting real unexpected arguments."""
        return {
            key: value for key, value in kwargs.items() if key != "additionalProperty"
        }

    def _set_public_signature(
        function: Callable[..., object],
        *,
        required_keyword_only: set[str] | None = None,
        keyword_only_after_first: bool = False,
    ) -> None:
        """Hide internal kwargs from schema generation and tighten required fields."""
        required_keyword_only = required_keyword_only or set()
        function_signature = signature(function)
        parameters = [
            parameter
            for parameter in function_signature.parameters.values()
            if parameter.name != "_kwargs"
        ]
        updated_parameters: list[Parameter] = []
        for index, parameter in enumerate(parameters):
            updated_parameter = parameter
            if parameter.name in required_keyword_only:
                updated_parameter = updated_parameter.replace(
                    kind=Parameter.KEYWORD_ONLY,
                    default=Parameter.empty,
                )
            elif keyword_only_after_first and index >= 1:
                updated_parameter = updated_parameter.replace(
                    kind=Parameter.KEYWORD_ONLY
                )
            updated_parameters.append(updated_parameter)

        setattr(
            function,
            "__signature__",
            Signature(parameters=updated_parameters),
        )

    async def ctl_finish(
        result_ref: str | None = None,
        error: str | None = None,
        **_kwargs: object,
    ) -> str:
        """Complete your task. If your task failed due to an unrecoverable
        internal error, provide the error message. Do NOT set error just
        because a task was difficult.

        Call this when you have finished your work and want to exit.
        If you spawned child agents, you MUST await or cancel all of
        them before calling ctl_finish.

        Args:
            result_ref: Optional path to a result file you produced.
            error: Optional error message for unrecoverable internal errors.
        """
        from anima.shell.ctl import ctl_finish as ctl_finish_impl

        if _unexpected_kwargs(_kwargs):
            return "Error: Invalid ctl_finish arguments"

        result = await ctl_finish_impl(
            instance_id,
            result_ref,
            response_text=agent_runner.last_response_text,
            error=error,
        )
        if isinstance(result, Failure):
            return f"Error: {result.failure().message}"
        if error:
            return f"Agent failed: {error}"
        return "Agent finished successfully"

    _set_public_signature(ctl_finish)

    async def ctl_request_approval(
        reason: str = "Approval requested by tool policy",
        **_kwargs: object,
    ) -> ApprovalToolPayload:
        """Pause and ask a human for permission before proceeding.

        Blocks until the human responds with approve or reject.
        Use when: about to perform a destructive action, confidence is
        low, or tool policy requires it.

        For waiting on child agents, use ctl_await instead.
        For waiting on external events, use ctl_suspend_until instead.

        Args:
            reason: Human-readable explanation of what you need approval for.
        """
        if _unexpected_kwargs(_kwargs):
            return {
                "approved": False,
                "note": "Error: Invalid ctl_request_approval arguments",
            }

        from anima.shell.ctl_approval import (
            clear_approval_response,
            ctl_request_approval as ctl_request_approval_impl,
        )
        from anima.shell.state import write_state

        approval_future = agent_runner.prepare_interactive_approval_future()

        result = await ctl_request_approval_impl(
            instance_id,
            reason,
            approval_event=agent_runner._approval_event,
            approval_future=approval_future,
            read_state_fn=read_state,
            clear_approval_response_fn=clear_approval_response,
            write_state_fn=write_state,
        )
        if isinstance(result, Failure):
            return {
                "approved": False,
                "note": f"Error: {result.failure().message}",
            }
        decision = result.unwrap()
        approved = cast(bool, decision["approved"])
        note = cast(str | None, decision["note"])
        return {
            "approved": approved,
            "note": note,
        }

    _set_public_signature(ctl_request_approval)

    async def ctl_spawn(
        persona_ref: str | None = None,
        task: str = "",
        lifecycle: str | None = None,
        context_mode: str = "isolated",
        max_turns: int | None = None,
        max_tokens: int | None = None,
        **_kwargs: object,
    ) -> str:
        """Start a child agent in the background. Returns immediately with child_id.

        You MUST call ctl_await with the returned child_id to get results.
        Do NOT fabricate or assume what the child returned.

        Use ONLY for 2+ parallel tasks. For a single task, use ctl_delegate.

        Pattern: id1 = ctl_spawn(task=A); id2 = ctl_spawn(task=B);
                 results = ctl_await(child_ids=[id1, id2])

        Args:
            task: Self-contained instruction for the child. Include all
                context — the child has no memory of your conversation.
            persona_ref: Persona identifier for the child agent. Accepts:
                - Relative file path: "personas/helper.json"
                - Registry spec_id: "python-executor" (resolved via larva if available)
                - None: inherit the parent's persona (default)
                Do NOT use absolute paths or add @ prefix.
            lifecycle: "ephemeral" (default, auto-exits on completion) or "daemon" (stays alive).
            context_mode: "isolated" (default, child starts fresh) or "shared" (child receives parent's conversation history).
            max_turns: Leave as None (unlimited). Do NOT set unless user asks.
            max_tokens: Leave as None (unlimited). Do NOT set unless user asks.
        """
        from anima.shell.ctl import ctl_spawn as ctl_spawn_impl

        if _unexpected_kwargs(_kwargs):
            return "Error: Invalid ctl_spawn_subagent arguments"

        if not task:
            return "Error: task is required"

        result = await ctl_spawn_impl(
            instance_id,
            persona_ref,
            task,
            context_mode=context_mode,
            lifecycle=lifecycle,
            max_turns=max_turns,
            max_tokens=max_tokens,
            mcp_servers=getattr(agent_runner, "mcp_servers", None),
        )
        if isinstance(result, Failure):
            error_msg = result.failure().message
            hint = ""
            if "not found" in error_msg.lower() and persona_ref:
                hint = (
                    f" Hint: '{persona_ref}' is not a local file. "
                    "If this is a registry spec_id, ensure larva MCP is connected "
                    "(--mcp 'larva mcp'). Use None to inherit parent's persona."
                )
            return f"Error: {error_msg}{hint}"
        return f"Spawned subagent: {result.unwrap()}"

    _set_public_signature(
        ctl_spawn,
        required_keyword_only={"task"},
        keyword_only_after_first=True,
    )

    # --- Subagent coordination tools ---

    async def ctl_delegate(
        task: str,
        persona_ref: str | None = None,
        context_mode: str | None = None,
        max_turns: int | None = None,
        max_tokens: int | None = None,
        timeout: float | None = None,
        max_response_chars: int | None = 4000,
        **_kwargs: object,
    ) -> str:
        """Give a task to a child agent and wait for the result.

        This is the simplest way to delegate work. Equivalent to
        ctl_spawn + ctl_await for a single child.

        For 2+ parallel tasks, use ctl_spawn + ctl_await instead.

        Args:
            task: Self-contained instruction for the child. Include all
                context — the child has no memory of your conversation.
            persona_ref: Persona identifier for the child agent. Accepts:
                - Relative file path: "personas/helper.json"
                - Registry spec_id: "python-executor" (resolved via larva if available)
                - None: inherit the parent's persona (default)
                Do NOT use absolute paths or add @ prefix.
            context_mode: "isolated" (default, child starts fresh) or "shared" (child receives parent's conversation history).
            max_turns: Leave as None (unlimited). Do NOT set unless user asks.
            max_tokens: Leave as None (unlimited). Do NOT set unless user asks.
            timeout: Max seconds to wait. None = wait indefinitely.
            max_response_chars: Truncate response to this length. Default 4000.
        """
        from anima.shell.ctl_delegate import ctl_delegate as ctl_delegate_impl

        if _unexpected_kwargs(_kwargs):
            return "Error: Invalid ctl_delegate arguments"

        result = await ctl_delegate_impl(
            instance_id, task, persona_ref, context_mode,
            max_turns, max_tokens, timeout, max_response_chars,
            mcp_servers=getattr(agent_runner, "mcp_servers", None),
        )
        if isinstance(result, Failure):
            error_msg = result.failure().message
            hint = ""
            if "not found" in error_msg.lower():
                ref = persona_ref or "(inherited from parent)"
                hint = (
                    f" Hint: persona '{ref}' is not a local file. "
                    "If this is a registry spec_id, ensure larva MCP is connected. "
                    "Use persona_ref=None to inherit parent's persona."
                )
            return f"Error: {error_msg}{hint}"
        return result.unwrap()

    async def ctl_await(
        child_ids: list[str],
        timeout: float | None = None,
        max_response_chars: int | None = 4000,
        **_kwargs: object,
    ) -> str:
        """Wait for spawned children to finish and return all results.

        Blocks until ALL children reach a terminal state (completed, failed,
        or cancelled). On timeout, returns results for finished children;
        still-running children are NOT cancelled — use ctl_cancel to clean
        them up if needed.

        To check progress without blocking, use ctl_poll.
        To debug a child's reasoning, use ctl_child_logs.

        Args:
            child_ids: List of child_id strings from ctl_spawn. Use the
                EXACT strings returned — do not modify or reconstruct them.
            timeout: Max seconds to wait. None = wait indefinitely.
            max_response_chars: Truncate each child's response. Default 4000.
        """
        from anima.shell.ctl_delegate import ctl_await as ctl_await_impl

        if _unexpected_kwargs(_kwargs):
            return "Error: Invalid ctl_await arguments"

        result = await ctl_await_impl(
            instance_id, child_ids, timeout, max_response_chars,
        )
        if isinstance(result, Failure):
            return f"Error: {result.failure().message}"
        return result.unwrap()

    async def ctl_poll(
        child_id: str,
        **_kwargs: object,
    ) -> str:
        """Check a child's current status without waiting.

        Returns immediately with one of: running, completed, failed, killed.
        Does NOT return the child's response — use ctl_await for that.
        To debug a child, use ctl_child_logs.

        Args:
            child_id: The child's instance_id from ctl_spawn.
        """
        from anima.shell.ctl_child import ctl_poll as ctl_poll_impl

        if _unexpected_kwargs(_kwargs):
            return "Error: Invalid ctl_poll arguments"

        result = await ctl_poll_impl(instance_id, child_id)
        if isinstance(result, Failure):
            return f"Error: {result.failure().message}"
        return result.unwrap()

    async def ctl_cancel(
        child_id: str,
        **_kwargs: object,
    ) -> str:
        """Cancel a running child agent. The child is terminated immediately.

        Use when a child is no longer needed (e.g., another child already
        provided the answer, or the parent task changed).
        You can still call ctl_child_logs after cancellation to inspect
        what the child did before it was cancelled.

        Args:
            child_id: The child's instance_id from ctl_spawn.
        """
        from anima.shell.ctl_child import ctl_cancel as ctl_cancel_impl

        if _unexpected_kwargs(_kwargs):
            return "Error: Invalid ctl_cancel arguments"

        result = await ctl_cancel_impl(instance_id, child_id)
        if isinstance(result, Failure):
            return f"Error: {result.failure().message}"
        return result.unwrap()

    async def ctl_child_logs(
        child_id: str,
        last_n: int | None = 10,
        **_kwargs: object,
    ) -> str:
        """Read a child agent's conversation log for debugging.

        Returns filtered log events (messages, responses, tool calls, errors).
        Works on children in any state (running, completed, failed, cancelled).
        Consumes your context budget — use only when needed.

        Args:
            child_id: The child's instance_id from ctl_spawn.
            last_n: Return last N events. Default 10. None for all (caution: large).
        """
        from anima.shell.ctl_child import ctl_child_logs as ctl_child_logs_impl

        if _unexpected_kwargs(_kwargs):
            return "Error: Invalid ctl_child_logs arguments"

        result = await ctl_child_logs_impl(instance_id, child_id, last_n)
        if isinstance(result, Failure):
            return f"Error: {result.failure().message}"
        return result.unwrap()

    async def ctl_switch_persona(
        persona_ref: str,
        **_kwargs: object,
    ) -> str:
        """Switch this agent's persona at the next turn boundary.

        The switch is cooperative: current turn completes normally, then the
        agent rebuilds with the new persona. Message history, turn counters,
        and token accounting are preserved.

        Only one pending switch is allowed. If a switch is already pending,
        this call is rejected.

        Args:
            persona_ref: Persona identifier to switch to. Accepts:
                - Relative file path: "personas/reviewer.json"
                - Registry spec_id: "code-reviewer" (if larva available)
                Do NOT use absolute paths or add @ prefix.
        """
        from anima.shell.ctl_spawn import _load_persona_from_ref

        if _unexpected_kwargs(_kwargs):
            return "Error: Invalid ctl_switch_persona arguments"

        if not persona_ref:
            return "Error: persona_ref is required"

        # Load and validate the persona spec
        # Resolution chain: file path → larva MCP → error
        persona_result = _load_persona_from_ref(persona_ref)
        if isinstance(persona_result, Failure):
            error = persona_result.failure()
            if "not found" in error.message:
                # File not found — try resolving as spec_id via larva MCP
                from anima.shell.larva_persona_resolver import (
                    resolve_persona_via_larva,
                )

                mcp_servers = getattr(agent_runner, "mcp_servers", None)
                larva_result = await resolve_persona_via_larva(
                    persona_ref, mcp_servers
                )
                if isinstance(larva_result, Failure):
                    return f"Error: {larva_result.failure().message}"
                persona_result = larva_result
            else:
                # Security or parse errors — do not attempt larva fallback
                return f"Error: {error.message}"

        persona = persona_result.unwrap()

        # Request the switch via the MCP server's single-pending slot
        mcp_server = agent_runner.mcp_server
        if mcp_server is None:
            return "Error: MCP server not available"

        accepted = mcp_server.request_persona_switch(persona, persona_ref)
        if not accepted:
            return "Error: A persona switch is already pending"

        return (
            f"Persona switched to: {persona.id}. "
            "Will take effect at next turn."
        )

    _set_public_signature(ctl_switch_persona)

    _set_public_signature(ctl_delegate)
    _set_public_signature(ctl_await)
    _set_public_signature(ctl_poll)
    _set_public_signature(ctl_cancel)
    _set_public_signature(ctl_child_logs)

    # Return as PydanticAI Tool objects
    from pydantic_ai import Tool

    # Note: ctl_spawn_subagent alias removed from LLM tool list to avoid
    # confusion (identical to ctl_spawn). Python import alias still works.
    # Note: ctl_fail merged into ctl_finish(error=...) parameter.
    # Note: ctl_suspend_until removed from LLM tool list (internal API remains).
    return [
        Tool(ctl_finish, name="ctl_finish"),
        Tool(ctl_request_approval, name="ctl_request_approval"),
        Tool(ctl_spawn, name="ctl_spawn"),
        Tool(ctl_delegate, name="ctl_delegate"),
        Tool(ctl_await, name="ctl_await"),
        Tool(ctl_poll, name="ctl_poll"),
        Tool(ctl_cancel, name="ctl_cancel"),
        Tool(ctl_child_logs, name="ctl_child_logs"),
        Tool(ctl_switch_persona, name="ctl_switch_persona"),
    ]


__all__ = ["create_ctl_tools"]
