"""Built-in ctl.* lifecycle tools: finish/fail/request_approval/spawn/suspend.

Adapter boundary note: ctl remains the transport/tool facade. Control-flow
orchestration ownership belongs in ``anima.app.control_service``.
"""

from datetime import datetime, timezone

from returns.result import Failure, Result, Success

from anima.core.lifecycle import LifecycleEvent, is_terminal, next_state
from anima.core.schemas import (
    AgentResult,
    AgentState,
    AgentStateData,
    AnimaError,
    InvalidInputError,
    LifecycleType,
)
from anima.shell import ctl_approval
from anima.shell.ctl_approval import ApprovalDecision
from anima.shell.ctl_spawn import ctl_spawn  # noqa: F401  # re-exported symbol
from anima.shell.ctl_spawn import ctl_spawn_subagent  # noqa: F401  # backward compat alias
from anima.shell.state import append_log, read_state, write_result, write_state


async def ctl_request_approval(
    instance_id: str,
    reason: str,
) -> Result[ApprovalDecision, AnimaError]:
    """Request approval and wait for decision.

    Compatibility source: tests patch ``anima.shell.ctl.write_state`` to
    validate interleaving behavior, so this wrapper injects ctl module
    state functions into the extracted approval workflow.
    """
    return await ctl_approval.ctl_request_approval(
        instance_id,
        reason,
        read_state_fn=read_state,
        clear_approval_response_fn=ctl_approval.clear_approval_response,
        write_state_fn=write_state,
    )


# @shell_complexity: Terminal/daemon state checking with logging
def _check_terminal_state(
    instance_id: str, state_data: AgentStateData
) -> Result[None, None]:
    """Check if agent is in terminal or daemon state and log if so.

    Args:
        instance_id: Agent instance ID.
        state_data: Current agent state.

    Returns:
        Success(None) if should ignore (terminal/daemon), Failure(None) if should proceed.
    """
    if state_data.lifecycle == LifecycleType.daemon:
        append_log(
            instance_id,
            "finish_ignored",
            {"reason": "daemon lifecycle ignores ctl_finish"},
        )
        return Success(None)

    if state_data.state in {AgentState.completed, AgentState.failed, AgentState.killed}:
        append_log(
            instance_id,
            "finish_ignored",
            {"reason": f"already in terminal state: {state_data.state.value}"},
        )
        return Success(None)

    return Failure(None)


def _transition_state(
    instance_id: str, event: LifecycleEvent
) -> Result[tuple[AgentStateData, AgentState], AnimaError]:
    """Read state and attempt lifecycle transition."""
    state_result = read_state(instance_id)
    if isinstance(state_result, Failure):
        return state_result

    state_data = state_result.unwrap()
    try:
        new_state = next_state(state_data.state, event)
    except InvalidInputError as exc:
        return Failure(
            AnimaError(
                code=1,
                message=(
                    f"Cannot apply {event.value} to agent in state "
                    f"'{state_data.state.value}': {exc}"
                ),
            )
        )

    return Success((state_data, new_state))


# @shell_complexity: Lifecycle transition with result write and logging
async def ctl_finish(
    instance_id: str,
    result_ref: str | None = None,
    response_text: str | None = None,
    error: str | None = None,
) -> Result[None, AnimaError]:
    """Finish agent execution, transitioning to completed or failed state.

    When ``error`` is provided, uses ``LifecycleEvent.fail`` instead of
    ``LifecycleEvent.finish`` and records the error on both the result
    and state objects.

    Args:
        instance_id: The agent's instance ID.
        result_ref: Optional reference to result file (LLM-facing parameter).
        response_text: Optional last response text from the agent. Internal
            parameter -- not exposed in the LLM tool schema. Passed by the
            agent runner to ensure result.json contains the full response
            on the first write, eliminating the race window where result.json
            exists without response_text.
        error: Optional error message. When provided, agent transitions to
            failed state instead of completed.
    """
    state_result = read_state(instance_id)
    if isinstance(state_result, Failure):
        return state_result
    state_data = state_result.unwrap()

    terminal_check = _check_terminal_state(instance_id, state_data)
    if isinstance(terminal_check, Success):
        return Success(None)

    # Guard 2: block finish if un-awaited children are still running
    if state_data.children:
        non_terminal_children = []
        for child_id in state_data.children:
            child_state_result = read_state(child_id)
            if isinstance(child_state_result, Success):
                child_state = child_state_result.unwrap()
                if not is_terminal(child_state.state):
                    non_terminal_children.append(child_id)
            # If child state can't be read, skip (may have been cleaned up)
        if non_terminal_children:
            return Failure(
                AnimaError(
                    code=1,
                    message=(
                        f"Cannot finish: {len(non_terminal_children)} un-awaited children "
                        f"still running: {non_terminal_children}. Await or cancel them first."
                    ),
                )
            )

    event = LifecycleEvent.fail if error else LifecycleEvent.finish
    try:
        new_state = next_state(state_data.state, event)
    except InvalidInputError as exc:
        return Failure(
            AnimaError(
                code=1,
                message=f"Cannot finish agent in state '{state_data.state.value}': {exc}",
            )
        )

    result = AgentResult(
        result_ref=result_ref,
        final_state=new_state,
        token_usage=state_data.token_usage,
        response_text=response_text,
        error=error,
    )
    write_result_outcome = write_result(instance_id, result)
    if isinstance(write_result_outcome, Failure):
        return write_result_outcome

    state_data.state = new_state
    state_data.updated_at = datetime.now(timezone.utc).isoformat()
    state_data.result_ref = result_ref
    if error:
        state_data.error = error
        return write_state(instance_id, state_data, "failed", {"error": error})
    return write_state(instance_id, state_data, "finished", {"result_ref": result_ref})


# @shell_complexity: Lifecycle transition with error logging
async def ctl_fail(
    instance_id: str,
    error: str,
) -> Result[None, AnimaError]:
    """Fail agent execution, transitioning to failed state."""
    transition_result = _transition_state(instance_id, LifecycleEvent.fail)
    if isinstance(transition_result, Failure):
        return transition_result
    state_data, new_state = transition_result.unwrap()

    result = AgentResult(
        final_state=new_state,
        token_usage=state_data.token_usage,
        error=error,
    )
    write_result_outcome = write_result(instance_id, result)
    if isinstance(write_result_outcome, Failure):
        return write_result_outcome

    state_data.state = new_state
    state_data.updated_at = datetime.now(timezone.utc).isoformat()
    state_data.error = error
    return write_state(
        instance_id,
        state_data,
        "failed",
        {"error": error},
    )


# @shell_complexity: Lifecycle transition with optional resume time
async def ctl_suspend_until(
    instance_id: str,
    resume_after: str | None = None,
) -> Result[None, AnimaError]:
    """Suspend agent execution until a specified time."""
    transition_result = _transition_state(instance_id, LifecycleEvent.suspend)
    if isinstance(transition_result, Failure):
        return transition_result
    state_data, new_state = transition_result.unwrap()

    state_data.state = new_state
    state_data.updated_at = datetime.now(timezone.utc).isoformat()
    state_data.resume_after = resume_after
    return write_state(
        instance_id,
        state_data,
        "suspended",
        {"resume_after": resume_after} if resume_after else None,
    )
