"""Stage tools that persist exclusively to ``find_agent_v2_*`` tables."""

from __future__ import annotations

import json
from collections.abc import Callable, Iterable
from typing import Any

from find_agent_v2.providers import (
    fetch_details,
    fetch_portraits,
    search_internal,
    search_tikhub,
)
from find_agent_v2.service import get_find_agent_v2_service
from supply_agent.tools import tool
from supply_agent.tools.registry import ToolRegistry

ToolFn = Callable[..., Any]


def bound_candidate_tools(
    functions: tuple[ToolFn, ...], *, run_id: str, candidate_ids: list[int],
) -> tuple[ToolFn, ...]:
    """Physically constrain worker tools to one run and one candidate shard."""
    allowed = {int(value) for value in candidate_ids}
    output: list[ToolFn] = []
    for original in functions:
        name = str(getattr(original, "_tool_name", original.__name__))
        if name == "query_pending_candidates_v2":
            def make_query(fn_name: str, worker_run_id: str, worker_allowed: set[int]):
                @tool(name=fn_name, description="仅查询当前 Worker 分片中的待评估候选。")
                def bound_query(run_id: str, limit: int = 100) -> str:
                    if run_id != worker_run_id:
                        return json.dumps({"error": "run_id 不属于当前 Worker"}, ensure_ascii=False)
                    state = get_find_agent_v2_service().get_full_state(
                        run_id, limit=limit, pending_only=True,
                    )
                    state["candidates"] = [
                        item for item in state["candidates"]
                        if int(item["candidate_id"]) in worker_allowed
                    ]
                    return json.dumps(state, ensure_ascii=False, default=str)

                return bound_query

            output.append(make_query(name, run_id, allowed))
            continue

        def make_async(fn: ToolFn, fn_name: str, worker_run_id: str, worker_allowed: set[int]):
            @tool(name=fn_name, description=getattr(fn, "_tool_description", ""))
            async def bound_async(run_id: str, candidate_ids: list[int]) -> str:
                if run_id != worker_run_id:
                    return json.dumps({"error": "run_id 不属于当前 Worker"}, ensure_ascii=False)
                requested = [int(value) for value in candidate_ids]
                if not requested or not set(requested) <= worker_allowed:
                    return json.dumps({
                        "error": "candidate_ids 超出当前 Worker 分片",
                        "allowed_candidate_ids": sorted(worker_allowed),
                    }, ensure_ascii=False)
                return await fn(run_id=run_id, candidate_ids=requested)

            return bound_async

        def make_sync(fn: ToolFn, fn_name: str, worker_run_id: str, worker_allowed: set[int]):
            @tool(name=fn_name, description=getattr(fn, "_tool_description", ""))
            def bound_sync(run_id: str, items: list[dict[str, Any]]) -> str:
                if run_id != worker_run_id:
                    return json.dumps({"error": "run_id 不属于当前 Worker"}, ensure_ascii=False)
                requested = {int(item.get("candidate_id") or 0) for item in items}
                if not requested or not requested <= worker_allowed:
                    return json.dumps({
                        "error": "items 超出当前 Worker 分片",
                        "allowed_candidate_ids": sorted(worker_allowed),
                    }, ensure_ascii=False)
                return fn(run_id=run_id, items=items)

            return bound_sync

        wrapper: ToolFn
        if name in {"fetch_candidate_details_v2", "fetch_candidate_portraits_v2"}:
            wrapper = make_async(original, name, run_id, allowed)
        elif name == "evaluate_candidates_v2":
            wrapper = make_sync(original, name, run_id, allowed)
        else:
            wrapper = original
        output.append(wrapper)
    return tuple(output)


@tool
async def search_videos_v2(run_id: str, round_index: int, searches: list[dict[str, Any]]) -> str:
    """批量搜索并仅写入 find_agent_v2_search/candidate；searches 最多 6 项。"""
    if not searches or len(searches) > 6:
        return json.dumps({"error": "searches 必须为 1~6 项"}, ensure_ascii=False)
    service = get_find_agent_v2_service()
    outputs: list[dict[str, Any]] = []
    for raw_task in searches:
        keyword = str(raw_task.get("keyword") or "").strip()
        reason = str(raw_task.get("query_reason") or "").strip()
        provider = str(raw_task.get("provider") or "internal_keyword")
        if not keyword or not reason:
            outputs.append({"error": "keyword/query_reason 不能为空"})
            continue
        max_pages = max(1, min(int(raw_task.get("max_pages") or 1), 2))
        cursor: str | int = raw_task.get("cursor") or 0
        provider_search_id = str(raw_task.get("search_id") or "")
        backtrace = str(raw_task.get("backtrace") or "")
        for page_no in range(1, max_pages + 1):
            common = {
                "keyword": keyword,
                "content_type": str(raw_task.get("content_type") or "视频"),
                "sort_type": str(raw_task.get("sort_type") or "综合排序"),
                "publish_time": str(raw_task.get("publish_time") or "不限"),
                "min_duration_seconds": int(raw_task.get("min_duration_seconds") or 30),
            }
            if provider == "tikhub":
                payload = await search_tikhub(
                    **common,
                    cursor=int(cursor or 0),
                    filter_duration=str(raw_task.get("filter_duration") or "不限"),
                    search_id=provider_search_id,
                    backtrace=backtrace,
                )
            else:
                provider = "internal_keyword"
                payload = await search_internal(**common, cursor=str(cursor or "0"))
            saved = service.save_search(
                run_id=run_id,
                round_index=int(round_index),
                keyword=keyword,
                query_reason=reason,
                source_type=str(raw_task.get("source_type") or "mixed"),
                provider=provider,
                cursor=str(cursor),
                page_no=page_no,
                payload=payload,
            )
            outputs.append({
                "keyword": keyword,
                "provider": provider,
                "page_no": page_no,
                "error": payload.get("error"),
                "has_more": bool(payload.get("has_more")),
                "next_cursor": payload.get("next_cursor"),
                **saved,
            })
            if payload.get("error") or not payload.get("has_more"):
                break
            cursor = payload.get("next_cursor") or cursor
            provider_search_id = str(payload.get("search_id") or provider_search_id)
            backtrace = str(payload.get("backtrace") or backtrace)
    return json.dumps({"run_id": run_id, "searches": outputs}, ensure_ascii=False)


@tool
async def fetch_candidate_details_v2(run_id: str, candidate_ids: list[int]) -> str:
    """批量获取候选详情并仅写入 find_agent_v2_candidate/evidence；最多 8 项。"""
    service = get_find_agent_v2_service()
    candidates = service.candidate_inputs(run_id, candidate_ids[:8])
    payload = await fetch_details([item["aweme_id"] for item in candidates])
    service.save_details(run_id, list(payload.get("details") or []), list(payload.get("errors") or []))
    return json.dumps({
        "run_id": run_id,
        "success_count": int(payload.get("success_count") or len(payload.get("details") or [])),
        "failed_count": int(payload.get("failed_count") or len(payload.get("errors") or [])),
        "errors": payload.get("errors") or [],
    }, ensure_ascii=False)


@tool
async def fetch_candidate_portraits_v2(run_id: str, candidate_ids: list[int]) -> str:
    """批量获取候选双侧年龄画像并仅写入 find_agent_v2_candidate/evidence。"""
    service = get_find_agent_v2_service()
    candidates = service.candidate_inputs(run_id, candidate_ids[:8])
    payload = await fetch_portraits([{
            "aweme_id": item["aweme_id"],
            "author_sec_uid": item.get("author_sec_uid"),
        } for item in candidates])
    results = list(payload.get("results") or [])
    service.save_portraits(run_id, results)
    return json.dumps({"run_id": run_id, "count": len(results), "results": results}, ensure_ascii=False)


@tool
def evaluate_candidates_v2(run_id: str, items: list[dict[str, Any]]) -> str:
    """写入 R/E/S/V 与 primary/rejected；程序会对 primary 强制执行 P0 门禁。"""
    updated = get_find_agent_v2_service().evaluate(run_id, items)
    return json.dumps({"run_id": run_id, "updated": updated}, ensure_ascii=False)


@tool
def query_find_agent_v2_state(run_id: str, limit: int = 100) -> str:
    """查询完全隔离的 find_agent_v2 运行、搜索与候选状态。"""
    state = get_find_agent_v2_service().get_full_state(run_id, limit=limit)
    return json.dumps(state, ensure_ascii=False, default=str)


@tool
def query_pending_candidates_v2(run_id: str, limit: int = 100) -> str:
    """仅查询当前 run 尚未分池的 pending_evaluation 候选。"""
    state = get_find_agent_v2_service().get_full_state(
        run_id, limit=limit, pending_only=True,
    )
    return json.dumps(state, ensure_ascii=False, default=str)


SEARCH_TOOLS: tuple[ToolFn, ...] = (search_videos_v2, query_find_agent_v2_state)
EVIDENCE_TOOLS: tuple[ToolFn, ...] = (
    fetch_candidate_details_v2,
    fetch_candidate_portraits_v2,
    query_pending_candidates_v2,
)
EVALUATION_TOOLS: tuple[ToolFn, ...] = (
    evaluate_candidates_v2,
    query_pending_candidates_v2,
)
REPORT_TOOLS: tuple[ToolFn, ...] = (query_find_agent_v2_state,)


def build_tool_registry(functions: Iterable[ToolFn]) -> ToolRegistry:
    return ToolRegistry().from_decorated(*tuple(functions))
