"""Wizsearch-powered web search and page crawling tools.

These tools expose multi-engine search and web page crawling through the
optional ``wizsearch`` package.
"""

from __future__ import annotations

import asyncio
import logging
import os
from typing import TYPE_CHECKING, Any, TypeVar

from langchain_core.tools import BaseTool
from pydantic import Field

from soothe.utils.url_validation import validate_url

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
    from collections.abc import Awaitable

try:
    from wizsearch import PageCrawler, WizSearch, WizSearchConfig

    WIZSEARCH_AVAILABLE = True
except ImportError:  # pragma: no cover - optional dependency
    PageCrawler = None
    WizSearch = None
    WizSearchConfig = None
    WIZSEARCH_AVAILABLE = False


def _require_wizsearch() -> None:
    """Ensure optional wizsearch dependency is available."""
    if not WIZSEARCH_AVAILABLE:
        msg = "wizsearch package is not installed. Install it with `pip install soothe[wizsearch]`."
        raise ImportError(msg)


def _normalize_engines(engines: list[str] | str | None) -> list[str] | None:
    """Normalize engine list input from list, JSON string, or comma-separated string.

    LLMs sometimes pass a stringified JSON array (e.g. ``'["google", "bing"]'``)
    instead of an actual list.  We try ``json.loads`` first so the individual
    engine names are preserved correctly.
    """
    if engines is None:
        return None
    if isinstance(engines, list):
        normalized = [str(engine).strip() for engine in engines if str(engine).strip()]
        return normalized or None
    if isinstance(engines, str):
        import json

        try:
            parsed = json.loads(engines)
            if isinstance(parsed, list):
                normalized = [str(e).strip() for e in parsed if str(e).strip()]
                return normalized or None
        except (json.JSONDecodeError, ValueError):
            pass
        normalized = [part.strip() for part in engines.split(",") if part.strip()]
        return normalized or None
    return None


def _to_serializable_sources(result: object) -> list[dict[str, object]]:
    """Map wizsearch sources to plain dictionaries."""
    raw_sources = getattr(result, "sources", []) or []
    return [
        {
            "title": getattr(source, "title", ""),
            "url": getattr(source, "url", ""),
            "content": getattr(source, "content", ""),
        }
        for source in raw_sources
    ]


_T = TypeVar("_T")


def _run_coro(coro: Awaitable[_T]) -> _T:
    """Run an async coroutine from sync tool entrypoint."""
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        return asyncio.run(coro)
    if loop.is_running():
        msg = "Cannot run synchronous tool method inside an active asyncio event loop. Use async invocation instead."
        raise RuntimeError(msg)
    return loop.run_until_complete(coro)


def _maybe_apply_tavily_key() -> None:
    """Backfill TAVILY_API_KEY from alternate env name when present."""
    if os.environ.get("TAVILY_API_KEY"):
        return
    alt = os.environ.get("WIZSEARCH_TAVILY_API_KEY")
    if alt:
        os.environ["TAVILY_API_KEY"] = alt


class WizsearchSearchTool(BaseTool):
    """Multi-engine web search tool powered by wizsearch."""

    name: str = "wizsearch_search"
    description: str = (
        "Search the web with multiple engines using wizsearch. "
        "For time-sensitive queries (e.g., 'latest news', 'recent events'), "
        "first use the current_datetime tool to know today's date, then include appropriate "
        "time qualifiers (year, month) in your search query to get the most recent results. "
        "Inputs: `query` (required), `engines` (optional, omit to use configured defaults), "
        "`max_results_per_engine` (default: 10), and `timeout` seconds (default: 30). "
        "Returns query, answer, sources, response_time, and metadata."
    )
    default_max_results_per_engine: int = Field(default=10)
    default_timeout: int = Field(default=30)
    default_engines: list[str] = Field(default_factory=lambda: ["tavily", "duckduckgo"])
    config: dict[str, Any] = Field(default_factory=dict)
    _debug_mode: bool = False

    def __init__(self, **data: Any) -> None:
        """Initialize wizsearch search tool with optional config override.

        Args:
            **data: Tool configuration, including 'config' dict with
                'default_engines', 'max_results_per_engine', 'timeout', etc.
        """
        super().__init__(**data)
        # Override defaults from config if provided
        if self.config:
            if "default_engines" in self.config:
                self.default_engines = self.config["default_engines"]
            if "max_results_per_engine" in self.config:
                self.default_max_results_per_engine = self.config["max_results_per_engine"]
            if "timeout" in self.config:
                self.default_timeout = self.config["timeout"]
            # Extract debug mode from Soothe config
            self._debug_mode = self.config.get("debug", False)

    def _log_engine_diagnostics(self, engines: list[str]) -> None:
        """Log diagnostic information about search engine configurations."""
        # Check Tavily API key
        if "tavily" in engines:
            tavily_key = os.environ.get("TAVILY_API_KEY") or os.environ.get("WIZSEARCH_TAVILY_API_KEY")
            if tavily_key:
                logger.info("Tavily API key found (length: %d)", len(tavily_key))
            else:
                logger.warning("Tavily API key NOT FOUND - Tavily search will fail")

        # Log proxy configuration
        https_proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy")
        http_proxy = os.environ.get("HTTP_PROXY") or os.environ.get("http_proxy")
        if https_proxy or http_proxy:
            logger.info("Proxy configured: HTTPS=%s, HTTP=%s", https_proxy, http_proxy)

    def _validate_engine_config(self, engines: list[str]) -> list[dict[str, Any]]:
        """Validate configuration for requested engines and return warnings."""
        warnings = []

        for engine in engines:
            if engine == "tavily":
                key = os.environ.get("TAVILY_API_KEY") or os.environ.get("WIZSEARCH_TAVILY_API_KEY")
                if not key:
                    warnings.append(
                        {
                            "engine": engine,
                            "issue": "missing_api_key",
                            "message": "TAVILY_API_KEY not found in environment",
                            "action": "Set TAVILY_API_KEY or WIZSEARCH_TAVILY_API_KEY environment variable",
                        }
                    )

        return warnings

    def _build_result_payload(self, result: object) -> dict[str, object]:
        """Build a stable JSON-serializable output payload."""
        return {
            "query": getattr(result, "query", ""),
            "answer": getattr(result, "answer", None),
            "sources": _to_serializable_sources(result),
            "response_time": getattr(result, "response_time", None),
            "metadata": getattr(result, "metadata", None),
        }

    async def _perform_search(
        self,
        query: str,
        engines: list[str] | str | None = None,
        max_results_per_engine: int | None = None,
        timeout_seconds: int | None = None,
    ) -> dict[str, object]:
        from soothe.utils.output_capture import capture_subagent_output
        from soothe.utils.progress import emit_progress

        _require_wizsearch()
        _maybe_apply_tavily_key()

        config_kwargs: dict[str, object] = {
            "max_results_per_engine": max_results_per_engine or self.default_max_results_per_engine,
            "timeout": timeout_seconds or self.default_timeout,
            # Disable fail_silently when Soothe debug mode is enabled
            "fail_silently": not self._debug_mode,
        }
        normalized = _normalize_engines(engines) or self.default_engines
        config_kwargs["enabled_engines"] = normalized

        # Log debug mode status
        if self._debug_mode:
            logger.info("Wizsearch debug mode enabled: fail_silently=False, output_suppression=False")

        # Run diagnostics and validation
        self._log_engine_diagnostics(normalized)
        validation_warnings = self._validate_engine_config(normalized)
        for warning in validation_warnings:
            logger.warning("Engine %s: %s - %s", warning["engine"], warning["issue"], warning["message"])

        # Emit progress event before search
        emit_progress(
            {
                "type": "soothe.tool.search.started",
                "query": query,
                "engines": normalized,
            },
            logger,
        )

        try:
            # Suppress output unless in debug mode
            with capture_subagent_output("wizsearch", suppress=not self._debug_mode):
                searcher = WizSearch(config=WizSearchConfig(**config_kwargs))
                result = await searcher.search(query=query)

                # Log which engines succeeded/failed
                if hasattr(result, "metadata") and result.metadata:
                    engine_status = result.metadata.get("engine_status", {})
                    for engine_name, status in engine_status.items():
                        logger.debug("Engine %s: %s", engine_name, status)

                payload = self._build_result_payload(result)

                # Emit completion event
                emit_progress(
                    {
                        "type": "soothe.tool.search.completed",
                        "query": query,
                        "result_count": len(payload.get("sources", [])),
                        "response_time": payload.get("response_time"),
                    },
                    logger,
                )
                return payload
        except Exception:
            logger.warning("Search failed with engines %s, retrying with defaults", normalized, exc_info=True)

        try:
            config_kwargs["enabled_engines"] = self.default_engines
            with capture_subagent_output("wizsearch", suppress=not self._debug_mode):
                searcher = WizSearch(config=WizSearchConfig(**config_kwargs))
                result = await searcher.search(query=query)

                # Log which engines succeeded/failed
                if hasattr(result, "metadata") and result.metadata:
                    engine_status = result.metadata.get("engine_status", {})
                    for engine_name, status in engine_status.items():
                        logger.debug("Engine %s: %s", engine_name, status)

                payload = self._build_result_payload(result)

                # Emit completion event
                emit_progress(
                    {
                        "type": "soothe.tool.search.completed",
                        "query": query,
                        "result_count": len(payload.get("sources", [])),
                        "response_time": payload.get("response_time"),
                    },
                    logger,
                )
                return payload
        except Exception as exc:
            logger.exception("Search failed even with default engines")

            # Emit failure event with engine status if available
            emit_progress(
                {
                    "type": "soothe.tool.search.failed",
                    "query": query,
                    "error": str(exc),
                    "engines": normalized,
                    "engine_status": getattr(exc, "engine_status", {}),
                    "debug_mode": self._debug_mode,
                },
                logger,
            )
            return {
                "query": query,
                "answer": None,
                "sources": [],
                "response_time": None,
                "metadata": None,
                "error": str(exc),
            }

    def _run(
        self,
        query: str,
        engines: list[str] | str | None = None,
        max_results_per_engine: int | None = None,
        timeout_seconds: int | None = None,
    ) -> dict[str, object]:
        return _run_coro(
            self._perform_search(
                query=query,
                engines=engines,
                max_results_per_engine=max_results_per_engine,
                timeout_seconds=timeout_seconds,
            )
        )

    async def _arun(
        self,
        query: str,
        engines: list[str] | str | None = None,
        max_results_per_engine: int | None = None,
        timeout_seconds: int | None = None,
    ) -> dict[str, object]:
        return await self._perform_search(
            query=query,
            engines=engines,
            max_results_per_engine=max_results_per_engine,
            timeout_seconds=timeout_seconds,
        )


class WizsearchCrawlPageTool(BaseTool):
    """Web page crawler tool powered by wizsearch.

    Note: The crawler runs in headless mode by default (BrowserConfig default).
    This is not configurable through this tool interface as wizsearch's PageCrawler
    doesn't expose the headless parameter.
    """

    name: str = "wizsearch_crawl_page"
    description: str = (
        "Crawl and extract web page content using wizsearch (runs in headless mode). "
        "Inputs: `url` and optional `content_format` ('markdown', 'html', 'text') "
        "and `only_text` (default: false). Returns extracted content and metadata."
    )
    default_content_format: str = Field(default="markdown")
    config: dict[str, Any] = Field(default_factory=dict)

    async def _perform_crawl(
        self,
        url: str,
        content_format: str | None = None,
        *,
        only_text: bool = False,
    ) -> dict[str, object]:
        from soothe.utils.output_capture import capture_subagent_output
        from soothe.utils.progress import emit_progress

        _require_wizsearch()
        selected_format = (content_format or self.default_content_format).strip().lower()
        if selected_format not in {"markdown", "html", "text"}:
            selected_format = self.default_content_format

        # Validate URL
        validated_url, error = validate_url(url)
        if error:
            logger.warning("Invalid URL: %s", error)
            emit_progress(
                {
                    "type": "soothe.tool.crawl.failed",
                    "url": url,
                    "error": error,
                },
                logger,
            )
            return {
                "url": url,
                "content_format": selected_format,
                "only_text": only_text,
                "headless": True,
                "content": "",
                "content_length": 0,
                "error": error,
            }

        # Emit progress event before crawling
        emit_progress(
            {
                "type": "soothe.tool.crawl.started",
                "url": validated_url,
                "content_format": selected_format,
            },
            logger,
        )

        try:
            # Capture third-party library output (Crawl4AI initialization messages, etc.)
            with capture_subagent_output("wizsearch", suppress=True):
                # PageCrawler runs in headless mode by default (BrowserConfig default)
                crawler = PageCrawler(
                    url=validated_url,
                    content_format=selected_format,
                    only_text=only_text,
                )
                content = await crawler.crawl()

            payload = {
                "url": validated_url,
                "content_format": selected_format,
                "only_text": only_text,
                "headless": True,  # Always true - wizsearch default
                "content": content or "",
                "content_length": len(content or ""),
            }

            # Emit completion event
            emit_progress(
                {
                    "type": "soothe.tool.crawl.completed",
                    "url": validated_url,
                    "content_length": len(content or ""),
                },
                logger,
            )
        except Exception as exc:
            logger.exception("Crawl failed for %s", validated_url)

            # Add specific error type logging
            error_str = str(exc).lower()
            if "timeout" in error_str:
                logger.warning("Crawl timed out - consider increasing timeout or checking network")
            elif "connection" in error_str:
                logger.warning("Connection failed - check URL accessibility and proxy settings")
            elif "javascript" in error_str or "render" in error_str:
                logger.warning("JavaScript rendering issue - page may require JS execution")

            # Emit failure event
            emit_progress(
                {
                    "type": "soothe.tool.crawl.failed",
                    "url": validated_url,
                    "error": str(exc),
                    "error_type": type(exc).__name__,
                },
                logger,
            )
            return {
                "url": validated_url,
                "content_format": selected_format,
                "only_text": only_text,
                "headless": True,
                "content": "",
                "content_length": 0,
                "error": str(exc),
            }
        else:
            return payload

    def _run(
        self,
        url: str,
        content_format: str | None = None,
        *,
        only_text: bool = False,
    ) -> dict[str, object]:
        return _run_coro(
            self._perform_crawl(
                url=url,
                content_format=content_format,
                only_text=only_text,
            )
        )

    async def _arun(
        self,
        url: str,
        content_format: str | None = None,
        *,
        only_text: bool = False,
    ) -> dict[str, object]:
        return await self._perform_crawl(
            url=url,
            content_format=content_format,
            only_text=only_text,
        )


def create_wizsearch_tools(config: dict[str, Any] | None = None) -> list[BaseTool]:
    """Create wizsearch tool instances.

    Args:
        config: Optional configuration dict with keys:
            - default_engines: List of default search engines
            - max_results_per_engine: Max results per engine
            - timeout: Request timeout in seconds
            - headless: Run browser crawler in headless mode

    Returns:
        List containing wizsearch search and crawl tools.
    """
    config = config or {}
    return [
        WizsearchSearchTool(config=config),
        WizsearchCrawlPageTool(config=config),
    ]
