"""Ephemeral ACP execution helper for delegated jobs.

Runs a single prompt against an ACP agent in a fresh session and tears the
session/transport down afterwards. No session IDs are persisted.
"""

from __future__ import annotations

import asyncio
from collections.abc import Callable
from contextlib import suppress
from dataclasses import dataclass
from typing import Any

from otto.acp.client import ACPClient, ACPPromptDone, ACPTextChunk, ACPToolStatus
from otto.acp.orchestration_tools import ACPOrchestrationSnapshot
from otto.acp.registry import resolve_acp_agent
from otto.log import get_logger

_log = get_logger("otto.acp.executor")

DEFAULT_ACP_EXECUTION_TIMEOUT_SECONDS = 300


@dataclass(frozen=True, slots=True)
class ACPExecutionResult:
    text: str
    stop_reason: str | None = None
    signals: ACPOrchestrationSnapshot | None = None


async def execute_acp_prompt(
    model: str,
    prompt: str,
    *,
    cwd: str,
    timeout_seconds: int = DEFAULT_ACP_EXECUTION_TIMEOUT_SECONDS,
    mcp_servers: list[Any] | None = None,
    signal_snapshot: Callable[[], ACPOrchestrationSnapshot] | None = None,
    on_tool_status: Callable[[ACPToolStatus], None] | None = None,
) -> ACPExecutionResult:
    """Run *prompt* against the ACP agent for *model* and return the full text."""
    if timeout_seconds <= 0:
        raise ValueError("timeout_seconds must be > 0")

    agent_def = resolve_acp_agent(model)
    client = ACPClient(
        command=agent_def.command,
        env=agent_def.env or None,
        cwd=cwd,
    )

    session_id: str | None = None
    stop_reason: str | None = None
    text_parts: list[str] = []

    _log.info(
        "acp executor starting",
        model=model,
        cwd=cwd,
        timeout_seconds=timeout_seconds,
        command=agent_def.command,
    )

    async def _run() -> None:
        nonlocal session_id, stop_reason

        await client.start()
        session_id = await client.new_session(cwd=cwd, mcp_servers=mcp_servers)

        async for update in client.prompt(session_id, prompt):
            if isinstance(update, ACPTextChunk):
                text_parts.append(update.text)
                continue
            if isinstance(update, ACPToolStatus):
                _log.info(
                    "acp executor tool status",
                    model=model,
                    session_id=session_id,
                    name=update.name,
                    status=update.status,
                    call_id=update.call_id,
                )
                if on_tool_status is not None:
                    try:
                        on_tool_status(update)
                    except Exception:
                        _log.warning("acp tool status callback failed", exc_info=True)
                continue
            if isinstance(update, ACPPromptDone):
                stop_reason = update.stop_reason

    try:
        await asyncio.wait_for(_run(), timeout=timeout_seconds)
    except TimeoutError:
        _log.warning(
            "acp executor timed out",
            model=model,
            session_id=session_id,
            timeout_seconds=timeout_seconds,
        )
        if session_id and client.started:
            with suppress(Exception):
                await client.cancel(session_id)
        raise TimeoutError(f"ACP job timed out after {timeout_seconds}s") from None
    except Exception:
        _log.warning(
            "acp executor failed",
            model=model,
            session_id=session_id,
            command=agent_def.command,
            exc_info=True,
        )
        raise
    finally:
        try:
            await client.close()
        except Exception:
            _log.warning(
                "acp executor cleanup failed",
                model=model,
                session_id=session_id,
                exc_info=True,
            )

    text = "".join(text_parts)
    if not text.strip():
        text = "(sub-agent produced no text output)"

    signals = signal_snapshot() if signal_snapshot is not None else None

    _log.info(
        "acp executor completed",
        model=model,
        session_id=session_id,
        stop_reason=stop_reason,
        text_length=len(text),
    )
    return ACPExecutionResult(
        text=text,
        stop_reason=stop_reason,
        signals=signals,
    )
