"""Web tools: WebSearch and WebFetch."""

from __future__ import annotations

import re
from typing import Any, Optional

import httpx
from typeguard import typechecked

from backend.apps.agents.tools.base import BaseTool, ToolContext
from backend.apps.agents.tools.search.search_ddg import (
    DDGRateLimited,
    HTTP_TIMEOUT,
    USER_AGENT,
)
from backend.apps.agents.tools.fetch.html_to_text import html_to_text
from backend.apps.agents.tools.fetch.page_text import PageText, body_to_text, looks_like_pdf
from backend.apps.agents.tools.search.search_ddg import search_ddg as run_ddg_search
from backend.apps.agents.tools.ssrf_guard import DomainUnreachable, SSRFBlocked, safe_fetch

P_MAX_OUTPUT_BYTES = 250 * 1024  # ~250 KB covers ~95% of articles/wikis/docs.


def anthropic_web_search_is_reliable(*, uses_direct_anthropic_api: bool,
                                     is_pro: bool) -> bool:
    """Whether the CLI's built-in WebSearch is reliable enough to suppress the
    DuckDuckGo fallback. The built-in tool fires an aux `claude-haiku` call, and
    that call only authenticates when it reaches an ENTITLED Anthropic endpoint:

      - `uses_direct_anthropic_api`: the session is pinned to a direct Anthropic
        api-route model (base_url = api.anthropic.com with the user's own key),
        so the haiku call hits Anthropic directly and works.
      - `is_pro`: OpenSwarm Pro, entitled to the managed `anthropic` pool that
        9Router's `anthropic/*` route resolves to.

    A bare `anthropic_api_key` in settings is NOT sufficient: a SUBSCRIPTION-route
    Claude model (e.g. `opus-4-8`, route=None) still sends the haiku call through
    9Router to the managed pool, which 401s for non-Pro users ('Invalid bearer
    token, reset after ~2m'). Only a `*-api` route model talks to Anthropic
    directly. Everyone else keeps the free, always-working DDG path."""
    return bool(uses_direct_anthropic_api or is_pro)


@typechecked
def should_register_web_mcp(
    *,
    model: str,
    router_model_id: object,
    api_type: Optional[str],
    anthropic_api_key: Optional[str],
    connection_mode: str,
) -> bool:
    """True when the agent loop must register the DDG-backed openswarm-web MCP because the
    primary model has NO reliable native Anthropic web-search path. We prefer Anthropic's
    hosted search (return False) whenever it's actually reachable, and cascade through our own
    /api/web/search (Gemini -> OpenAI -> DuckDuckGo) otherwise. The three no-path cases:
    a non-Claude primary, a custom-provider session (ANTHROPIC_BASE_URL points at 9Router with
    no Claude connection), and a subscription-route Claude model on a non-Pro account (the
    built-in WebSearch's aux haiku call 401s). Pro pool is deliberately NOT counted for a
    non-Claude primary: spending it on WebSearch would drain the user's Claude turns."""
    from backend.apps.agents.providers.registry import find_builtin_model as find_builtin_model

    m = router_model_id if isinstance(router_model_id, str) else ""
    primary_is_claude = m.startswith("cc/") or (
        isinstance(router_model_id, str)
        and not router_model_id.startswith(("cc/", "cx/", "gc/", "ag/", "gemini/"))
        and api_type == "anthropic"
    )
    is_custom_session = api_type == "custom"
    web_model_entry = find_builtin_model(model)
    uses_direct_anthropic_api = (
        web_model_entry is not None
        and web_model_entry.get("route") == "api"
        and web_model_entry.get("api") == "anthropic"
        and bool(anthropic_api_key)
    )
    has_anthropic_path = (
        not is_custom_session
        and primary_is_claude
        and anthropic_web_search_is_reliable(
            uses_direct_anthropic_api=uses_direct_anthropic_api,
            is_pro=(connection_mode in ("openswarm-pro", "free-trial")),
        )
    )
    return not has_anthropic_path


def p_truncate(text: str, limit: int = P_MAX_OUTPUT_BYTES) -> str:
    if len(text) > limit:
        return text[:limit] + "\n... (output truncated)"
    return text


class WebSearchTool(BaseTool):
    name = "WebSearch"
    description = (
        "Search the web using DuckDuckGo and return titles, URLs, and "
        "snippets for the top results."
    )

    def get_schema(self) -> dict:
        return {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "The search query.",
                },
                "num_results": {
                    "type": "integer",
                    "description": "Maximum number of results to return (default 5).",
                    "default": 5,
                },
            },
            "required": ["query"],
            "additionalProperties": False,
        }

    async def execute(self, input_data: dict, context: ToolContext) -> list[dict]:
        query: str = input_data["query"]
        num_results: int = input_data.get("num_results", 5)

        try:
            results = await self.search_ddg(query, num_results)
            if not results:
                return [{"type": "text", "text": f"No search results found for: {query}"}]
            return [{"type": "text", "text": results}]
        except DDGRateLimited:
            # "Wait and retry" was a dead end: the 202 is an anti-automation challenge on the client, not a cooldown, so an immediate retry gets the same answer.
            return [{"type": "text", "text": (
                "DuckDuckGo answered its bot challenge (HTTP 202) instead of results, on both "
                "its html and lite frontends. Retrying the same query will not clear it; use "
                "another search source."
            )}]
        except Exception as exc:
            return [{"type": "text", "text": f"Web search error: {exc}"}]

    @staticmethod
    async def search_ddg(query: str, num_results: int) -> str:
        return await run_ddg_search(query, num_results)


class WebFetchTool(BaseTool):
    name = "WebFetch"
    description = (
        "Fetch the contents of a URL and return the extracted text. "
        "HTML is stripped to plain text. Output capped at ~250 KB."
    )

    def get_schema(self) -> dict:
        return {
            "type": "object",
            "properties": {
                "url": {
                    "type": "string",
                    "description": "The URL to fetch.",
                },
                "prompt": {
                    "type": "string",
                    "description": "Optional prompt/context describing what information to look for.",
                },
            },
            "required": ["url"],
            "additionalProperties": False,
        }

    async def execute(self, input_data: dict, context: ToolContext) -> list[dict]:
        page = await self.fetch_page(input_data["url"], input_data.get("prompt"))
        return [{"type": "text", "text": page.text}]

    @staticmethod
    async def fetch_page(url: str, prompt: str | None = None) -> PageText:
        """The page as readable text, plus WHAT it was, so callers can tell a
        JS wall (worth another tier) from a PNG (nothing left to try)."""
        try:
            resp = await safe_fetch(
                url,
                method="GET",
                headers={"User-Agent": USER_AGENT},
                timeout=HTTP_TIMEOUT,
            )
            resp.raise_for_status()
        except DomainUnreachable as exc:
            return PageText(text=f"Could not reach {url}: {exc}", kind="error")
        except SSRFBlocked as exc:
            return PageText(text=f"Refused to fetch {url}: {exc}", kind="error")
        except httpx.HTTPStatusError as exc:
            return PageText(text=f"HTTP error {exc.response.status_code} fetching {url}", kind="error")
        except Exception as exc:
            return PageText(text=f"Error fetching {url}: {exc}", kind="error")

        content_type = resp.headers.get("content-type", "")
        # A PDF's content-type often says html, so check the magic bytes before trusting the header.
        is_pdf = looks_like_pdf(content_type, resp.content)
        is_html = not is_pdf and ("html" in content_type or resp.text.strip().startswith("<!"))

        if is_html:
            body = PageText(text=html_to_text(resp.text), kind="html")
        else:
            body = body_to_text(content_type, resp.content, resp.text)

        header = f"Contents of {url}:"
        if prompt:
            header += f"\n(Looking for: {prompt})"

        return PageText(text=f"{header}\n\n{p_truncate(body.text)}", kind=body.kind)
