"""Web Search plugin — multi-provider search with automatic fallback."""

from __future__ import annotations

import asyncio
import logging
import time
from typing import Any

import httpx

from agent_scout.plugins.base import Plugin, PluginConfig, PluginParam, ToolSpec

logger = logging.getLogger(__name__)


class SearchResult:
    """A single search result."""

    __slots__ = ("title", "url", "snippet", "source")

    def __init__(self, title: str, url: str, snippet: str, source: str) -> None:
        self.title = title
        self.url = url
        self.snippet = snippet
        self.source = source

    def to_dict(self) -> dict[str, str]:
        return {
            "title": self.title,
            "url": self.url,
            "snippet": self.snippet,
            "source": self.source,
        }


class RateLimiter:
    """Simple token-bucket rate limiter per provider."""

    def __init__(self, max_calls: int, period_seconds: float) -> None:
        self._max_calls = max_calls
        self._period = period_seconds
        self._calls: list[float] = []

    def allow(self) -> bool:
        now = time.monotonic()
        self._calls = [t for t in self._calls if now - t < self._period]
        if len(self._calls) < self._max_calls:
            self._calls.append(now)
            return True
        return False

    @property
    def calls_remaining(self) -> int:
        now = time.monotonic()
        self._calls = [t for t in self._calls if now - t < self._period]
        return max(0, self._max_calls - len(self._calls))


class SearchProvider:
    """Base class for search providers."""

    name: str = ""

    def __init__(self, rate_limiter: RateLimiter | None = None) -> None:
        self._rate_limiter = rate_limiter or RateLimiter(max_calls=30, period_seconds=60)

    async def search(self, query: str, max_results: int = 5) -> list[SearchResult]:
        raise NotImplementedError

    def is_available(self) -> bool:
        return self._rate_limiter.allow()


class DuckDuckGoProvider(SearchProvider):
    """Free search via duckduckgo-search library."""

    name = "duckduckgo"

    async def search(self, query: str, max_results: int = 5) -> list[SearchResult]:
        from duckduckgo_search import DDGS

        results: list[SearchResult] = []
        try:
            ddgs = DDGS()
            raw = await asyncio.to_thread(ddgs.text, query, max_results=max_results)
            for r in raw:
                results.append(
                    SearchResult(
                        title=r.get("title", ""),
                        url=r.get("href", ""),
                        snippet=r.get("body", ""),
                        source=self.name,
                    )
                )
        except Exception as e:
            logger.warning("DuckDuckGo search failed: %s", e)
            raise
        return results


class TavilyProvider(SearchProvider):
    """Tavily search API (1000 free/month)."""

    name = "tavily"

    def __init__(self, api_key: str, rate_limiter: RateLimiter | None = None) -> None:
        super().__init__(rate_limiter)
        self._api_key = api_key

    def is_available(self) -> bool:
        return bool(self._api_key) and super().is_available()

    async def search(self, query: str, max_results: int = 5) -> list[SearchResult]:
        async with httpx.AsyncClient() as client:
            resp = await client.post(
                "https://api.tavily.com/search",
                json={
                    "api_key": self._api_key,
                    "query": query,
                    "max_results": max_results,
                },
                timeout=15,
            )
            resp.raise_for_status()
            data = resp.json()

        results: list[SearchResult] = []
        for r in data.get("results", []):
            results.append(
                SearchResult(
                    title=r.get("title", ""),
                    url=r.get("url", ""),
                    snippet=r.get("content", ""),
                    source=self.name,
                )
            )
        return results


class SerpAPIProvider(SearchProvider):
    """SerpAPI search (100 free/month)."""

    name = "serpapi"

    def __init__(self, api_key: str, rate_limiter: RateLimiter | None = None) -> None:
        super().__init__(rate_limiter)
        self._api_key = api_key

    def is_available(self) -> bool:
        return bool(self._api_key) and super().is_available()

    async def search(self, query: str, max_results: int = 5) -> list[SearchResult]:
        async with httpx.AsyncClient() as client:
            resp = await client.get(
                "https://serpapi.com/search",
                params={
                    "api_key": self._api_key,
                    "q": query,
                    "num": max_results,
                    "engine": "google",
                },
                timeout=15,
            )
            resp.raise_for_status()
            data = resp.json()

        results: list[SearchResult] = []
        for r in data.get("organic_results", []):
            results.append(
                SearchResult(
                    title=r.get("title", ""),
                    url=r.get("link", ""),
                    snippet=r.get("snippet", ""),
                    source=self.name,
                )
            )
        return results


class BraveSearchProvider(SearchProvider):
    """Brave Search API."""

    name = "brave"

    def __init__(self, api_key: str, rate_limiter: RateLimiter | None = None) -> None:
        super().__init__(rate_limiter)
        self._api_key = api_key

    def is_available(self) -> bool:
        return bool(self._api_key) and super().is_available()

    async def search(self, query: str, max_results: int = 5) -> list[SearchResult]:
        async with httpx.AsyncClient() as client:
            resp = await client.get(
                "https://api.search.brave.com/res/v1/web/search",
                params={"q": query, "count": max_results},
                headers={
                    "X-Subscription-Token": self._api_key,
                    "Accept": "application/json",
                },
                timeout=15,
            )
            resp.raise_for_status()
            data = resp.json()

        results: list[SearchResult] = []
        for r in data.get("web", {}).get("results", []):
            results.append(
                SearchResult(
                    title=r.get("title", ""),
                    url=r.get("url", ""),
                    snippet=r.get("description", ""),
                    source=self.name,
                )
            )
        return results


def _deduplicate(results: list[SearchResult]) -> list[SearchResult]:
    """Remove duplicate results based on URL."""
    seen: set[str] = set()
    unique: list[SearchResult] = []
    for r in results:
        normalized = r.url.rstrip("/").lower()
        if normalized not in seen:
            seen.add(normalized)
            unique.append(r)
    return unique


class WebSearchPlugin(Plugin):
    """Multi-provider web search with automatic fallback chain.

    Provider priority: DuckDuckGo (free) → Tavily → SerpAPI → Brave.
    Falls back automatically if the primary provider fails or is rate-limited.
    """

    def __init__(self, config: PluginConfig | None = None) -> None:
        super().__init__(config)
        self._providers: list[SearchProvider] = []

    @property
    def name(self) -> str:
        return "web_search"

    @property
    def description(self) -> str:
        return "Search the web using multiple providers with automatic fallback"

    def get_tools(self) -> list[ToolSpec]:
        return [
            ToolSpec(
                name="web_search",
                description=(
                    "Search the web for information. Returns titles, URLs, and snippets. "
                    "Use this for any factual lookup, price research, or current information."
                ),
                parameters=[
                    PluginParam(name="query", description="Search query", required=True),
                    PluginParam(
                        name="max_results",
                        description="Maximum number of results",
                        param_type="number",
                        default=5,
                    ),
                ],
            ),
        ]

    async def on_load(self) -> None:
        """Build the provider chain from config."""
        settings = self._config.settings

        # Primary: DuckDuckGo (always available, free)
        self._providers.append(DuckDuckGoProvider())

        # Fallbacks (only if API keys are configured)
        if tavily_key := settings.get("tavily_api_key"):
            self._providers.append(TavilyProvider(tavily_key))

        if serpapi_key := settings.get("serpapi_api_key"):
            self._providers.append(SerpAPIProvider(serpapi_key))

        if brave_key := settings.get("brave_api_key"):
            self._providers.append(BraveSearchProvider(brave_key))

    def set_providers(self, providers: list[SearchProvider]) -> None:
        """Override the provider chain (useful for testing)."""
        self._providers = providers

    async def execute(self, tool_name: str, tool_input: dict[str, Any]) -> str:
        if tool_name != "web_search":
            return f"Unknown tool: {tool_name}"

        query = tool_input.get("query", "")
        max_results = int(tool_input.get("max_results", 5))

        if not query:
            return "Error: No search query provided"

        results = await self._search_with_fallback(query, max_results)

        if not results:
            return "No search results found."

        # Format results as readable text for the LLM
        lines: list[str] = []
        for i, r in enumerate(results, 1):
            lines.append(f"{i}. {r.title}")
            lines.append(f"   URL: {r.url}")
            lines.append(f"   {r.snippet}")
            lines.append("")

        return "\n".join(lines).strip()

    async def _search_with_fallback(self, query: str, max_results: int) -> list[SearchResult]:
        """Try each provider in order until one succeeds."""
        all_results: list[SearchResult] = []

        for provider in self._providers:
            if not provider.is_available():
                logger.info("Provider %s is rate-limited, skipping", provider.name)
                continue

            try:
                results = await provider.search(query, max_results)
                if results:
                    all_results.extend(results)
                    break  # Got results, stop fallback
            except Exception:
                logger.warning("Provider %s failed, trying next", provider.name, exc_info=True)
                continue

        return _deduplicate(all_results)[:max_results]
