"""Approval control helpers for ``ctl.*`` lifecycle tools.

Extracted from ``shell/ctl.py`` to keep control flows by concern.
"""

import asyncio
from datetime import datetime, timezone
from typing import Any, cast

from collections.abc import Callable

from returns.result import Failure, Result, Success

from anima.core.lifecycle import LifecycleEvent, next_state
from anima.core.schemas import AgentState, AgentStateData, AnimaError
from anima.core.schemas import InvalidInputError
from anima.shell.state import (
    clear_approval_response,
    read_approval_response,
    read_state,
    write_state,
)

ApprovalDecision = dict[str, bool | str | None]


# @shell_complexity: Approval workflow with state read, lifecycle transition validation, and result handling
async def ctl_request_approval(
    instance_id: str,
    reason: str,
    approval_event: asyncio.Event | None = None,
    approval_future: asyncio.Future[ApprovalDecision] | None = None,
    read_state_fn: Callable[
        [str], Result[Any, Any]
    ] = read_state,  # justified: compatibility seam for ctl monkeypatching
    clear_approval_response_fn: Callable[
        [str], Result[Any, Any]
    ] = clear_approval_response,  # justified: compatibility seam for ctl monkeypatching
    write_state_fn: Callable[
        [str, Any, str, dict | None], Result[Any, Any]
    ] = write_state,  # justified: compatibility seam for ctl monkeypatching
) -> Result[ApprovalDecision, AnimaError]:
    """Request approval and block until decision resolves.

    Args:
        instance_id: The agent instance identifier.
        reason: Human-readable reason for requiring approval.

    Returns:
        Success({"approved": bool, "note": str | None}) or Failure.
    """
    state_result = read_state_fn(instance_id)
    if isinstance(state_result, Failure):
        return state_result

    state_data = cast(AgentStateData, state_result.unwrap())
    try:
        new_state = next_state(state_data.state, LifecycleEvent.request_approval)
    except InvalidInputError as exc:
        return Failure(
            AnimaError(
                code=1,
                message=(
                    "Cannot request approval for agent in state "
                    f"'{state_data.state.value}': {exc}"
                ),
            )
        )

    clear_response_outcome = clear_approval_response_fn(instance_id)
    if isinstance(clear_response_outcome, Failure):
        return clear_response_outcome

    state_data.state = new_state
    state_data.updated_at = datetime.now(timezone.utc).isoformat()

    write_state_outcome = write_state_fn(
        instance_id,
        state_data,
        "approval_requested",
        {"reason": reason},
    )
    if isinstance(write_state_outcome, Failure):
        return write_state_outcome

    return await _wait_for_approval_decision(
        instance_id,
        approval_event=approval_event,
        approval_future=approval_future,
    )


# @shell_complexity: Poll-based wait with async task management handles multiple terminal states
async def _wait_for_approval_decision(
    instance_id: str,
    poll_interval_seconds: float = 0.1,
    approval_event: asyncio.Event | None = None,
    approval_future: asyncio.Future[ApprovalDecision] | None = None,
) -> Result[ApprovalDecision, AnimaError]:
    """Block until approval resolves and return decision payload."""
    try:
        while True:
            if approval_future is not None:
                if approval_future.done():
                    return Success(approval_future.result())
                try:
                    decision = await asyncio.wait_for(
                        asyncio.shield(approval_future),
                        timeout=poll_interval_seconds,
                    )
                    return Success(decision)
                except TimeoutError:
                    pass

            state_result = read_state(instance_id)
            if isinstance(state_result, Failure):
                return state_result

            state_data = state_result.unwrap()
            if state_data.state == AgentState.awaiting_approval:
                if approval_event is None:
                    await asyncio.sleep(poll_interval_seconds)
                else:
                    try:
                        await asyncio.wait_for(
                            approval_event.wait(),
                            timeout=poll_interval_seconds,
                        )
                    except TimeoutError:
                        continue
                    approval_event.clear()
                continue

            if state_data.state == AgentState.running:
                response_result = read_approval_response(instance_id)
                if isinstance(response_result, Success):
                    return response_result
                return Success({"approved": True, "note": None})

            if state_data.state == AgentState.failed:
                return Success(
                    {
                        "approved": False,
                        "note": state_data.error,
                    }
                )

            if state_data.state == AgentState.killed:
                return Success({"approved": False, "note": None})

            return Failure(
                AnimaError(
                    code=1,
                    message=(
                        "Approval wait aborted: "
                        f"agent entered unexpected state '{state_data.state.value}'"
                    ),
                )
            )
    except asyncio.CancelledError:
        return Failure(
            AnimaError(
                code=1,
                message=(
                    "Approval wait cancelled before decision was received "
                    f"for agent '{instance_id}'"
                ),
            )
        )
