"""Pausing a reply to ask the person reading it something.

Three features turn out to be one mechanism. A command that needs approving, a
question the model wants answered, and "this reply is waiting for you" are all:
stop the generation, put an interactive block in the bubble, wait for a POST,
carry on. So there is one primitive, and approval is a shape of question rather
than a separate machine.

Two things about where it sits matter.

**It pauses a round, not a call.** A round's tool calls run together under a
semaphore, and parking four coroutines on four separate answers inside that
gather would queue them behind each other invisibly -- and the reader would get
four cards, answerable in any order, for commands whose order matters. So one
card describes everything in the round that needs a decision, and the calls that
survive it run concurrently exactly as they did before.

**Stop has to keep working.** `generation.cancel` is read in one place, between
streamed chunks, and there are no chunks while paused. Rather than a second
poller, `generation.request_stop` resolves the pause directly; see the comment
there. Nothing in this module reaches back into `services.generation`, which is
what keeps it testable on its own.
"""

from __future__ import annotations

import asyncio
import time
from dataclasses import dataclass, field
from typing import TYPE_CHECKING

if TYPE_CHECKING:  # pragma: no cover - annotation only
    from lembas.services.generation import Generation

KIND_APPROVAL = "approval"
KIND_QUESTION = "question"

# How a pause ended.
ALLOW = "allow"
ALLOW_ALWAYS = "allow_always"
DENY = "deny"
ANSWER = "answer"
CANCELLED = "cancelled"  # Stop was pressed while the card was showing
EXPIRED = "expired"  # nobody answered in time

# Outcomes that mean "go ahead".
PERMITTED = (ALLOW, ALLOW_ALWAYS)

# A card offering more than this many buttons is a card nobody reads.
MAX_OPTIONS = 6

# And more than this many questions at once is a form, not a conversation. A
# model that wants twenty answers should ask for four and then ask again with
# what it learned.
MAX_QUESTIONS = 8


@dataclass(frozen=True)
class Item:
    """One thing being asked about: a single question, or one command.

    `index` is the position of the *call* in its round, so an answer can be
    matched back to the call it belongs to -- the tool turns have to line up
    with the assistant turn's `tool_calls`, or an endpoint pairs the wrong
    result with the right id. Several items can share an index, because one
    `ask_user` call may carry several questions.

    `key` identifies this item within the card, and is what the form field is
    named after. Stable and opaque: a question's own text would make a terrible
    field name, and its position alone would collide across calls.
    """

    index: int
    key: str
    kind: str
    tool_name: str
    title: str
    detail: str = ""
    reason: str = ""
    options: tuple[str, ...] = ()
    allow_free_text: bool = True


@dataclass
class Interruption:
    """A reply, stopped, waiting for one answer to cover every item."""

    id: str
    items: tuple[Item, ...]
    expires_at: float = 0.0
    _future: asyncio.Future | None = field(default=None, repr=False, compare=False)

    @property
    def kind(self) -> str:
        return KIND_QUESTION if any(i.kind == KIND_QUESTION for i in self.items) else KIND_APPROVAL

    def resolve(self, outcome: str, *, answers: dict[str, str] | None = None) -> bool:
        """Complete this pause. Idempotent -- a second answer is ignored.

        Returns whether this call was the one that answered it, which is what
        the endpoint reports back: a card answered twice (two tabs, a double
        click) should say so rather than pretend.
        """
        if self._future is None or self._future.done():
            return False
        self._future.set_result(Reply(outcome=outcome, answers=dict(answers or {})))
        return True


@dataclass(frozen=True)
class Reply:
    """How a card was answered.

    `answers` is keyed by `Item.key`, so a card carrying four questions comes
    back as four answers in one go. An approval carries none: the verdict is
    the whole of it.
    """

    outcome: str
    answers: dict[str, str] = field(default_factory=dict)

    @property
    def permitted(self) -> bool:
        return self.outcome in PERMITTED

    @property
    def ended(self) -> bool:
        """Whether this outcome means the whole reply should stop."""
        return self.outcome == CANCELLED

    def answer_to(self, item: Item) -> str:
        return (self.answers.get(item.key) or "").strip()


def build(
    interaction_id: str, items: list[Item] | tuple[Item, ...], *, timeout: float
) -> Interruption:
    """An interruption with its future attached, ready to be waited on."""
    return Interruption(
        id=interaction_id,
        items=tuple(items),
        expires_at=time.monotonic() + timeout,
        _future=asyncio.get_running_loop().create_future(),
    )


async def wait_for(
    generation: Generation, interruption: Interruption, *, timeout: float
) -> Reply:
    """Park the generation on this interruption until somebody answers.

    Sets `generation.pending` and touches, so the follower sends the card on its
    next frame; clears both in `finally`, so answering makes it disappear. The
    time spent here accumulates on `generation.waited` and is taken off the
    reply's wall-clock budget -- a reader who thinks for ten minutes about one
    command should not thereby spend the whole allowance.
    """
    generation.pending = interruption
    generation.touch()
    started = time.monotonic()
    try:
        return await asyncio.wait_for(asyncio.shield(interruption._future), timeout)
    except TimeoutError:
        return Reply(outcome=EXPIRED)
    finally:
        generation.waited += time.monotonic() - started
        generation.pending = None
        generation.touch()


def summarise(items: tuple[Item, ...]) -> str:
    """What to show in the status line while the card is up."""
    if not items:
        return ""
    if items[0].kind == KIND_QUESTION:
        return "Waiting for your answer…" if len(items) == 1 else "Waiting for your answers…"
    if len(items) == 1:
        return f"Waiting for you to allow {items[0].tool_name}…"
    return f"Waiting for you to allow {len(items)} actions…"


__all__ = [
    "ALLOW",
    "ALLOW_ALWAYS",
    "ANSWER",
    "CANCELLED",
    "DENY",
    "EXPIRED",
    "KIND_APPROVAL",
    "KIND_QUESTION",
    "MAX_OPTIONS",
    "MAX_QUESTIONS",
    "PERMITTED",
    "Interruption",
    "Item",
    "Reply",
    "build",
    "summarise",
    "wait_for",
]
