"""Autonomous-agent endpoint: POST /agent/run.

Synchronous wrapper around :class:`praxia.agent.AutonomousAgent` and
:class:`praxia.agent.CommandedAgent` so a thin client (desktop / mobile)
can ask the server to "do this task" and get back the final answer + tool
trace without orchestrating Python locally.

This is the data-plane companion to :mod:`praxia.server.routers.threads`:
when a client posts a user message into a thread and wants an agent
reply, it calls this endpoint, then appends the returned text as an
assistant message (or passes ``thread_id`` so the server does both in
one round trip).

Streaming + WebSocket variants land in a later phase.
"""
from __future__ import annotations

import logging
import time
import uuid
from pathlib import Path
from typing import Any

from pydantic import BaseModel

_log = logging.getLogger(__name__)


# Module-level models so FastAPI / Pydantic introspection works cleanly
class AgentRunRequest(BaseModel):
    prompt: str
    thread_id: str | None = None      # if set, append both user msg + reply
    org_id: str = "default-org"
    model: str = "claude"
    # Optional: a smaller / cheaper LLM used for "scout" sub-calls —
    # query decomposition, grounding-verifier claim extraction, task
    # classification. Defaults to `model` (one LLM for everything).
    # Picking a smaller model here saves cost and latency on the
    # exploratory steps without hurting final-answer quality.
    scout_model: str | None = None
    # Optional: absolute path to a workspace directory. When set, the
    # agent gets Codex-style file tools (read_file / write_file /
    # edit_file / list_files / delete_file) scoped strictly to that
    # directory. The desktop app supplies this when the user picks a
    # Workspace folder. Path-traversal escapes are rejected inside
    # praxia.agent.file_tools.
    workspace_root: str | None = None
    max_steps: int = 8
    verified: bool = False            # use CommandedAgent if True
    max_verify_rounds: int = 3
    memory_dir: str | None = None     # override server's default


class AgentRunResponse(BaseModel):
    text: str
    tool_calls: list[dict[str, Any]] = []
    usage: dict[str, int] = {}
    steps: int = 0
    stopped_reason: str = "completed"
    # Commander-only:
    verdict_decision: str | None = None
    verdict_groundedness: float | None = None
    citations: list[str] = []
    rounds: int | None = None
    # If thread_id was supplied, the new message ids
    user_message_id: str | None = None
    assistant_message_id: str | None = None
    # Workspace-scoped file writes the agent wants to do but hasn't —
    # the host must show these to the user and call /workspace/apply
    # for each approved op.
    pending_file_operations: list[dict[str, Any]] = []


class ApplyFileOpRequest(BaseModel):
    """Body for POST /workspace/apply — execute one previously-queued
    file operation after the user approved it."""
    workspace_root: str
    op: dict[str, Any]


def build_router(*, current_user: Any, storage: Path):
    from fastapi import APIRouter, Depends, HTTPException

    chats_root = Path(storage) / "chats"

    # --- thread persistence (lightweight clone — keeps this router self-contained) ----

    def _append_to_thread(
        user_id: str,
        thread_id: str,
        role: str,
        content: str,
        metadata: dict[str, Any] | None = None,
    ) -> str:
        import json
        d = chats_root / user_id
        d.mkdir(parents=True, exist_ok=True)
        p = d / f"{thread_id}.json"
        if not p.exists():
            raise HTTPException(404, f"Thread not found: {thread_id}")
        data = json.loads(p.read_text(encoding="utf-8"))
        msg_id = uuid.uuid4().hex
        data["messages"].append({
            "id": msg_id,
            "role": role,
            "content": content,
            "timestamp": time.time(),
            "metadata": metadata or {},
        })
        data["updated_at"] = time.time()
        if not data.get("title") and role == "user":
            data["title"] = content[:80].replace("\n", " ").strip()
        p.write_text(
            json.dumps(data, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )
        return msg_id

    # --- routes --------------------------------------------------------

    router = APIRouter()

    @router.post("/agent/run", response_model=AgentRunResponse)
    def run_agent(req: AgentRunRequest, user=Depends(current_user)):
        if not req.prompt.strip():
            raise HTTPException(400, "Empty prompt")

        # Lazy imports — keeps server router cheap when agent isn't called
        try:
            from praxia.agent import AutonomousAgent, CommandedAgent
            from praxia.core.llm import LLM
        except ImportError as e:  # pragma: no cover
            raise HTTPException(500, f"Praxia agent not available: {e}")

        memory_dir = req.memory_dir or str(Path(storage))

        try:
            llm = LLM(req.model)
        except Exception as e:
            raise HTTPException(500, f"LLM init failed: {e}")

        # Optional scout LLM — used by the decomposer + verifier when
        # CommandedAgent is enabled below. If not provided (or set to the
        # same string as `model`), the main LLM doubles as scout.
        scout_llm = None
        if req.scout_model and req.scout_model != req.model:
            try:
                scout_llm = LLM(req.scout_model)
            except Exception as e:  # pragma: no cover
                # Soft-fail: a bad scout_model shouldn't kill the request,
                # just fall back to the main LLM for sub-calls.
                _log.warning("scout LLM init failed (%s); using main LLM", e)
                scout_llm = None

        # Workspace-scoped file tools — opt-in via workspace_root.
        # Resolved + validated inside praxia.agent.file_tools so a
        # malformed path returns HTTP 400 here rather than blowing up
        # inside the agent loop later.
        extra_tools = []
        pending_file_ops: list[dict[str, Any]] = []
        if req.workspace_root:
            try:
                from praxia.agent.file_tools import workspace_tools
                # Default to require_confirmation=True — file writes
                # never touch disk during the agent run; they queue
                # into pending_file_ops for the user to approve.
                tools_dict = workspace_tools(
                    req.workspace_root,
                    require_confirmation=True,
                    pending_sink=pending_file_ops,
                )
                extra_tools = list(tools_dict.values())
            except Exception as e:
                raise HTTPException(400, f"Invalid workspace_root: {e}")

        inner = AutonomousAgent(
            user_id=user.id,
            role=user.role,
            org_id=req.org_id,
            llm=llm,
            memory_dir=memory_dir,
            max_steps=max(1, int(req.max_steps)),
            extra_tools=extra_tools or None,
        )

        # If thread_id supplied, persist user message first (so the agent
        # sees it as user-said, and so retries pick it up).
        user_msg_id = None
        if req.thread_id:
            user_msg_id = _append_to_thread(
                user.id, req.thread_id, "user", req.prompt,
            )

        # Execute
        try:
            if req.verified:
                agent = CommandedAgent(
                    inner,
                    scout_llm=scout_llm,
                    max_verify_rounds=max(1, int(req.max_verify_rounds)),
                    require_citations=True,
                )
                cresult = agent.run(req.prompt)
                final_text = cresult.answer
                resp = AgentRunResponse(
                    text=final_text,
                    tool_calls=[],
                    usage=cresult.usage,
                    steps=sum((r.inner_result.steps if r.inner_result else 0) for r in cresult.rounds),
                    stopped_reason=cresult.stopped_reason,
                    verdict_decision=cresult.verdict.decision,
                    verdict_groundedness=cresult.verdict.groundedness,
                    citations=list(cresult.citations),
                    rounds=len(cresult.rounds),
                    pending_file_operations=list(pending_file_ops),
                )
            else:
                result = inner.run(req.prompt)
                resp = AgentRunResponse(
                    text=result.final_text,
                    tool_calls=[
                        {
                            "name": tc.name,
                            "ok": tc.ok,
                            "arguments_preview": tc.arguments_text[:200],
                            "result_preview": tc.result_text[:200],
                        }
                        for tc in result.tool_calls
                    ],
                    usage=result.usage,
                    steps=result.steps,
                    stopped_reason=result.stopped_reason,
                    pending_file_operations=list(pending_file_ops),
                )
                final_text = result.final_text
        except Exception as e:
            _log.exception("Agent run failed")
            raise HTTPException(500, f"Agent run failed: {e}")

        # Persist assistant reply
        if req.thread_id:
            asst_msg_id = _append_to_thread(
                user.id, req.thread_id, "assistant", final_text,
                metadata={"verified": req.verified, "model": req.model},
            )
            resp.user_message_id = user_msg_id
            resp.assistant_message_id = asst_msg_id

        return resp

    @router.post("/workspace/apply")
    def apply_workspace_op(req: ApplyFileOpRequest, user=Depends(current_user)):
        """Execute one previously-queued file operation.

        The agent run produced a ``pending_file_operations`` list. The
        frontend showed each one to the user with a diff preview. For
        every op the user clicked "Apply" on, the frontend calls this
        endpoint. Path scope is re-validated here — the user's
        approval doesn't override the workspace boundary, and
        ``apply_pending_op`` rejects anything that resolves outside.
        """
        try:
            from praxia.agent.file_tools import apply_pending_op
        except ImportError as e:  # pragma: no cover
            raise HTTPException(500, f"file_tools unavailable: {e}")

        auth_audit = getattr(user, "_audit_channel", None)
        try:
            # AuthManager's audit channel — pass through if reachable.
            from praxia.auth.manager import AuthManager  # noqa: F401
            # The audit instance lives on the request-scoped auth
            # manager. We approximate by importing the storage path.
        except Exception:  # pragma: no cover
            auth_audit = None

        result = apply_pending_op(
            req.workspace_root,
            req.op,
            actor_id=user.id,
            audit_record=auth_audit,
        )
        if "error" in result:
            raise HTTPException(400, result["error"])
        return result

    return router
