import asyncio
from dataclasses import dataclass
from typing import Any

from pydantic_ai import Agent, RunContext
from pydantic_ai.format_prompt import format_as_xml
from pydantic_ai.output import ToolOutput
from pydantic_graph import BaseNode, GraphRunContext

from haiku.rag.research.common import get_model, log
from haiku.rag.research.dependencies import ResearchDependencies
from haiku.rag.research.models import ResearchReport, SearchAnswer
from haiku.rag.research.prompts import SEARCH_AGENT_PROMPT
from haiku.rag.research.state import ResearchDeps, ResearchState


@dataclass
class SearchDispatchNode(BaseNode[ResearchState, ResearchDeps, ResearchReport]):
    provider: str
    model: str

    async def run(
        self, ctx: GraphRunContext[ResearchState, ResearchDeps]
    ) -> BaseNode[ResearchState, ResearchDeps, ResearchReport]:
        state = ctx.state
        deps = ctx.deps
        if not state.sub_questions:
            from haiku.rag.research.nodes.evaluate import EvaluateNode

            return EvaluateNode(self.provider, self.model)

        # Take up to max_concurrency questions and answer them concurrently
        take = max(1, state.max_concurrency)
        batch: list[str] = []
        while state.sub_questions and len(batch) < take:
            batch.append(state.sub_questions.pop(0))

        async def answer_one(sub_q: str) -> SearchAnswer | None:
            log(
                deps.console,
                f"\n[bold cyan]🔍 Searching & Answering:[/bold cyan] {sub_q}",
            )
            agent = Agent(
                model=get_model(self.provider, self.model),
                output_type=ToolOutput(SearchAnswer, max_retries=3),
                instructions=SEARCH_AGENT_PROMPT,
                retries=3,
                deps_type=ResearchDependencies,
            )

            @agent.tool
            async def search_and_answer(
                ctx2: RunContext[ResearchDependencies], query: str, limit: int = 5
            ) -> str:
                search_results = await ctx2.deps.client.search(query, limit=limit)
                expanded = await ctx2.deps.client.expand_context(search_results)

                entries: list[dict[str, Any]] = [
                    {
                        "text": chunk.content,
                        "score": score,
                        "document_uri": (
                            chunk.document_title or chunk.document_uri or ""
                        ),
                    }
                    for chunk, score in expanded
                ]
                if not entries:
                    return f"No relevant information found in the knowledge base for: {query}"

                return format_as_xml(entries, root_tag="snippets")

            agent_deps = ResearchDependencies(
                client=deps.client, context=state.context, console=deps.console
            )
            try:
                result = await agent.run(sub_q, deps=agent_deps)
            except Exception as e:
                log(deps.console, f"[red]Search failed:[/red] {e}")
                return None

            return result.output

        answers = await asyncio.gather(*(answer_one(q) for q in batch))
        for ans in answers:
            if ans is None:
                continue
            state.context.add_qa_response(ans)
            if deps.console:
                preview = ans.answer[:150] + ("…" if len(ans.answer) > 150 else "")
                log(deps.console, f"   [green]✓[/green] {preview}")

        return SearchDispatchNode(self.provider, self.model)
