"""Runs resource — execute agents and stream results."""

from __future__ import annotations

from collections.abc import Callable, Generator
from typing import TYPE_CHECKING, Any, cast

from .._streaming import RunStream
from .._types import PermissionModeResponse, PermissionRequest, Run, RunFile, SyncPage
from ._utils import _build_params

_list = list  # preserve builtin; shadowed by .list() method

if TYPE_CHECKING:
    from .._http import HTTPClient


class Runs:
    """client.runs — execute agents, stream events, manage runs."""

    def __init__(self, http: HTTPClient):
        self._http = http

    def create(
        self,
        *,
        message: str,
        teammate_id: int | None = None,
        tools: list[str] | None = None,
        stream: bool = True,
        name: str | None = None,
        instructions: str | None = None,
        user_id: str | None = None,
        metadata: dict | None = None,
        memory: bool = True,
        history: bool = True,
        human_in_the_loop: bool = False,
        permission_mode: str = "autonomous",
    ) -> RunStream | Run:
        """Create and execute a run.

        With stream=True (default): returns iterable RunStream of events.
        With stream=False: returns Run immediately (status="running").
            Poll GET /runs/{id} until status is terminal to get output.

        Set human_in_the_loop=True to enable interactive features
        (clarifying questions, tool approval, plan mode).
        """
        body: dict = {"message": message, "stream": stream}
        if teammate_id is not None:
            body["teammate_id"] = teammate_id
        if tools is not None:
            body["tools"] = tools
        if name is not None:
            body["name"] = name
        if instructions is not None:
            body["instructions"] = instructions
        if user_id is not None:
            body["user_id"] = user_id
        if metadata is not None:
            body["metadata"] = metadata
        body["memory"] = memory
        body["history"] = history
        if human_in_the_loop:
            body["human_in_the_loop"] = True
        if permission_mode != "autonomous":
            body["permission_mode"] = permission_mode

        if stream:
            resp = self._http.stream("POST", "/runs", json=body)
            return RunStream(resp)

        resp = self._http.request("POST", "/runs", json=body)
        return Run.from_dict(resp.json())

    def poll(self, run_id: int, *, interval: float = 2.0, timeout: float = 300.0) -> Run:
        """Poll until the run reaches a terminal status. Returns the completed Run.

        For runs with human_in_the_loop=True, use wait() instead — it handles
        awaiting_approval pauses via callbacks so the run can continue.
        """
        import time as _time

        from .._exceptions import APIError

        _TERMINAL = {"completed", "failed", "cancelled"}
        deadline = _time.monotonic() + timeout
        while True:
            try:
                run = self.get(run_id)
            except APIError:
                if _time.monotonic() >= deadline:
                    raise TimeoutError(f"Run {run_id} did not complete within {timeout}s") from None
                _time.sleep(interval)
                continue
            if run.status in _TERMINAL:
                return run
            if _time.monotonic() >= deadline:
                raise TimeoutError(f"Run {run_id} did not complete within {timeout}s")
            _time.sleep(interval)

    def wait(
        self,
        run_id: int,
        *,
        on_approval: Callable[[PermissionRequest], str] | None = None,
        on_question: Callable[[PermissionRequest], dict[str, str]] | None = None,
        interval: float = 2.0,
        timeout: float = 300.0,
    ) -> Run:
        """Wait for a run to complete, handling human-in-the-loop pauses via callbacks.

        Like poll(), but also handles awaiting_approval status:
        - Tool approvals: calls on_approval(req) → "allow" or "deny"
        - AskUserQuestion: calls on_question(req) → {question_text: answer} dict

        Without callbacks, raises RuntimeError if the run pauses for input.

        Usage:
            run = client.runs.wait(
                run.id,
                on_question=lambda req: {"Which segment?": "enterprise"},
                on_approval=lambda req: "allow",
            )

        For plan mode, check req.is_plan_approval and req.plan_text:
            def handle_question(req):
                if req.is_plan_approval:
                    print(req.plan_text)
                    return {"Plan Approval": "Approve"}
                return {}  # shouldn't happen

            run = client.runs.wait(run.id, on_question=handle_question)
        """
        import time as _time

        from .._exceptions import APIError

        _TERMINAL = {"completed", "failed", "cancelled"}
        deadline = _time.monotonic() + timeout

        while True:
            try:
                run = self.get(run_id)
            except APIError:
                if _time.monotonic() >= deadline:
                    raise TimeoutError(f"Run {run_id} did not complete within {timeout}s") from None
                _time.sleep(interval)
                continue

            if run.status in _TERMINAL:
                return run

            if run.status == "awaiting_approval":
                pending = [r for r in self.permissions(run_id) if r.status == "pending"]
                for req in pending:
                    if req.tool_name == "AskUserQuestion":
                        if on_question is None:
                            raise RuntimeError(
                                f"Run {run_id} is waiting for a question response "
                                f"(tool: AskUserQuestion). Pass on_question= to handle it, "
                                "or use runs.answer() manually."
                            )
                        answers = on_question(req)
                        self.answer(run_id, answers=answers)
                    else:
                        if on_approval is None:
                            raise RuntimeError(
                                f"Run {run_id} is waiting for tool approval "
                                f"({req.tool_name!r}). Pass on_approval= to handle it, "
                                "or use runs.approve() manually."
                            )
                        decision = on_approval(req)
                        self.approve(run_id, request_id=req.request_id, decision=decision)

            if _time.monotonic() >= deadline:
                raise TimeoutError(f"Run {run_id} did not complete within {timeout}s")
            _time.sleep(interval)

    def create_and_wait(
        self,
        *,
        message: str,
        teammate_id: int | None = None,
        tools: list[str] | None = None,
        name: str | None = None,
        instructions: str | None = None,
        user_id: str | None = None,
        metadata: dict | None = None,
        memory: bool = True,
        history: bool = True,
        human_in_the_loop: bool = False,
        permission_mode: str = "autonomous",
        on_approval: Callable[[PermissionRequest], str] | None = None,
        on_question: Callable[[PermissionRequest], dict[str, str]] | None = None,
        poll_interval: float = 2.0,
        poll_timeout: float = 300.0,
    ) -> Run:
        """Create a run and wait until it completes. Returns the finished Run.

        Pass on_approval= and on_question= to handle human-in-the-loop pauses inline.
        Without callbacks, HITL runs will raise RuntimeError when they pause for input.
        """
        run = self.create(
            message=message,
            teammate_id=teammate_id,
            tools=tools,
            stream=False,
            name=name,
            instructions=instructions,
            user_id=user_id,
            metadata=metadata,
            memory=memory,
            history=history,
            human_in_the_loop=human_in_the_loop,
            permission_mode=permission_mode,
        )
        return self.wait(
            cast(Run, run).id,
            on_approval=on_approval,
            on_question=on_question,
            interval=poll_interval,
            timeout=poll_timeout,
        )

    def reply_and_wait(
        self,
        run_id: int,
        *,
        message: str,
        on_approval: Callable[[PermissionRequest], str] | None = None,
        on_question: Callable[[PermissionRequest], dict[str, str]] | None = None,
        poll_interval: float = 2.0,
        poll_timeout: float = 300.0,
    ) -> Run:
        """Send a follow-up and wait until it completes. Returns the finished Run."""
        run = self.reply(run_id, message=message, stream=False)
        return self.wait(
            cast(Run, run).id,
            on_approval=on_approval,
            on_question=on_question,
            interval=poll_interval,
            timeout=poll_timeout,
        )

    def stream_text(
        self,
        *,
        message: str,
        teammate_id: int | None = None,
        tools: list[str] | None = None,
        name: str | None = None,
        instructions: str | None = None,
        user_id: str | None = None,
        metadata: dict | None = None,
        memory: bool = True,
        history: bool = True,
        human_in_the_loop: bool = False,
        permission_mode: str = "autonomous",
    ) -> Generator[str, None, None]:
        """Create a streaming run and yield only text delta strings.

        Usage:
            for chunk in client.runs.stream_text(message="Summarize news"):
                print(chunk, end="", flush=True)
        """
        from ..streaming import TextDeltaEvent

        stream = self.create(
            message=message,
            teammate_id=teammate_id,
            tools=tools,
            stream=True,
            name=name,
            instructions=instructions,
            user_id=user_id,
            metadata=metadata,
            memory=memory,
            history=history,
            human_in_the_loop=human_in_the_loop,
            permission_mode=permission_mode,
        )
        run_stream = cast(RunStream, stream)
        with run_stream:
            for event in run_stream:
                if isinstance(event, TextDeltaEvent):
                    yield event.delta

    def list(
        self,
        *,
        teammate_id: int | None = None,
        user_id: str | None = None,
        status: str | None = None,
        limit: int = 20,
        starting_after: int | None = None,
    ) -> SyncPage[Run]:
        params = _build_params(
            teammate_id=teammate_id,
            user_id=user_id,
            status=status,
            limit=limit,
            starting_after=starting_after,
        )
        resp = self._http.request("GET", "/runs", params=params)
        body = resp.json()

        def _fetch_next(**kw: object) -> SyncPage[Run]:
            return self.list(teammate_id=teammate_id, user_id=user_id, status=status, **kw)  # type: ignore[arg-type]

        return SyncPage(
            data=[Run.from_dict(d) for d in body["data"]],
            has_more=body["has_more"],
            _fetch_next=_fetch_next,
        )

    def get(self, run_id: int) -> Run:
        resp = self._http.request("GET", f"/runs/{run_id}")
        return Run.from_dict(resp.json())

    def reply(
        self,
        run_id: int,
        *,
        message: str,
        stream: bool = True,
    ) -> RunStream | Run:
        """Follow-up message on an existing run. Creates a new run ID.

        With stream=True (default): returns iterable RunStream of events.
        With stream=False: returns Run immediately (status="running").
            Poll GET /runs/{id} until status is terminal to get output.
        """
        body = {"message": message, "stream": stream}
        if stream:
            resp = self._http.stream("POST", f"/runs/{run_id}/reply", json=body)
            return RunStream(resp)
        resp = self._http.request("POST", f"/runs/{run_id}/reply", json=body)
        return Run.from_dict(resp.json())

    def cancel(self, run_id: int) -> Run:
        resp = self._http.request("POST", f"/runs/{run_id}/cancel")
        return Run.from_dict(resp.json())

    def update_permission_mode(
        self,
        run_id: int,
        *,
        permission_mode: str,
    ) -> PermissionModeResponse:
        """Change permission mode mid-run.

        Switches between 'autonomous', 'approval', and 'plan'.
        When switching to 'autonomous', all pending permission requests are auto-approved.
        """
        resp = self._http.request(
            "PATCH",
            f"/runs/{run_id}/permission-mode",
            json={"permission_mode": permission_mode},
        )
        return PermissionModeResponse.from_dict(resp.json())

    def permissions(self, run_id: int) -> _list[PermissionRequest]:
        """List tool permission requests for a run."""
        resp = self._http.request("GET", f"/runs/{run_id}/permissions")
        return [PermissionRequest.from_dict(d) for d in resp.json()]

    def answer(self, run_id: int, *, answers: dict[str, str]) -> dict[str, Any]:
        """Submit an answer to an agent's AskUserQuestion.

        Use this when the run is paused waiting for user input (AskUserQuestion).
        The answers dict maps question text (q["question"]) to the selected option label.

        Returns {"status": "ok", "resumed": bool}. When resumed is True, the run
        has been queued to continue from the point it paused.

        Raises ConflictError (409) if the run is terminal (completed, failed, cancelled).
        """
        resp = self._http.request("POST", f"/runs/{run_id}/answer", json={"answers": answers})
        result: dict[str, Any] = resp.json()
        return result

    def approve(
        self,
        run_id: int,
        *,
        request_id: str,
        decision: str = "allow",
        remember: bool = False,
    ) -> PermissionRequest:
        """Approve or deny a pending tool permission request."""
        body = {"request_id": request_id, "decision": decision, "remember": remember}
        resp = self._http.request("POST", f"/runs/{run_id}/approve", json=body)
        return PermissionRequest.from_dict(resp.json())

    def list_files(self, run_id: int) -> _list[RunFile]:
        """List files generated by a run."""
        resp = self._http.request("GET", f"/runs/{run_id}/files")
        return [RunFile.from_dict(f) for f in resp.json()]

    def download_file(self, run_id: int, filename: str) -> bytes:
        """Download a file generated by a run. Returns raw file bytes."""
        resp = self._http.request("GET", f"/runs/{run_id}/files/{filename}/download")
        return resp.content
