"""Human-in-the-loop decision polling for factory runs."""

from __future__ import annotations

import time
from typing import TYPE_CHECKING

from .data_types import AgentCall, DecisionOption, PlanOutput
from .tracer import Tracer, connect_db

if TYPE_CHECKING:
    from .runner import PhaseHandle, Run


class DecisionCanceled(Exception):
    """Raised when a pending decision is canceled before an answer arrives."""


class DecisionTimeout(Exception):
    """Raised when wait_for_decision exceeds timeout_sec."""


def wait_for_decision(
    tracer: Tracer,
    decision_id: str,
    poll_sec: float = 2.0,
    timeout_sec: float | None = None,
) -> dict:
    """Poll the trace db until the decision leaves pending."""
    deadline = None if timeout_sec is None else time.monotonic() + timeout_sec
    while True:
        conn = connect_db(tracer.db_path)
        try:
            row = conn.execute(
                "SELECT decision_id, adw_id, phase, question, options, free_text,"
                " context, status, answer_value, answer_text, answered_by,"
                " created_at, answered_at FROM decisions WHERE decision_id=?",
                (decision_id,),
            ).fetchone()
        finally:
            conn.close()

        if row is None:
            raise ValueError(f"unknown decision_id {decision_id}")

        status = row[7]
        if status == "pending":
            if deadline is not None and time.monotonic() >= deadline:
                raise DecisionTimeout(decision_id)
            time.sleep(poll_sec)
            continue

        decision = tracer.decision_get(decision_id)
        assert decision is not None
        if status == "canceled":
            raise DecisionCanceled(decision_id)
        return decision


def ask_human(
    tracer: Tracer,
    adw_id: str,
    question: str,
    options: list[dict] | None = None,
    free_text: bool = False,
    context: str = "",
    phase: str | None = None,
    poll_sec: float = 2.0,
    timeout_sec: float | None = None,
) -> dict:
    """Request a human decision and block until it is answered."""
    decision_id = tracer.decision_request(
        adw_id, question, options or [], free_text=free_text, context=context, phase=phase,
    )
    return wait_for_decision(
        tracer, decision_id, poll_sec=poll_sec, timeout_sec=timeout_sec,
    )


class TooManyHumanDecisions(RuntimeError):
    """Raised when a planning phase requests more owner decisions than allowed."""


def resolve_plan_decisions(
    run: "Run",
    ph: "PhaseHandle",
    call: AgentCall,
    max_rounds: int = 3,
    poll_sec: float = 2.0,
) -> PlanOutput:
    """Run a planner call, blocking on genuine owner forks until resolved."""
    base_prompt = call.prompt
    prompt = base_prompt
    answered: list[str] = []
    decision_rounds = 0

    while True:
        current = call if prompt == base_prompt else call.model_copy(update={"prompt": prompt})
        output = ph.call(current)
        if not isinstance(output, PlanOutput):
            raise TypeError(f"expected PlanOutput, got {type(output).__name__}")
        if output.human_decision is None:
            return output

        decision_rounds += 1
        if decision_rounds > max_rounds:
            raise TooManyHumanDecisions(
                f"phase {ph.phase.params.name!r} exceeded {max_rounds} human decision round(s)",
            )

        req = output.human_decision
        options = [
            {"value": opt.value, "label": opt.label or opt.value, "recommended": opt.recommended}
            for opt in req.options
        ]
        answer = ask_human(
            run.tracer,
            run.adw_id,
            req.question,
            options=options or None,
            free_text=req.free_text,
            context=req.context,
            phase=ph.phase.params.name,
            poll_sec=poll_sec,
        )
        chosen = answer.get("answer_value") or answer.get("answer_text") or ""
        answered.append(f"## Human decision\nQ: {req.question}\nA: {chosen}\n")
        prompt = base_prompt + "\n\n" + "\n".join(answered)
