"""Research I/O helpers — search, fetch, LLM completions, and synthesis.

Split from the monolithic ``research.py`` for maintainability.
"""

from __future__ import annotations

import asyncio
import logging
import textwrap
from typing import Any

from timmy.research.coordinator import _FETCH_TOP_N, _SYNTHESIS_MAX_TOKENS

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Query formulation (Step 2)
# ---------------------------------------------------------------------------


async def _formulate_queries(topic: str, template_context: str, n: int = 8) -> list[str]:
    """Use the local LLM to generate targeted search queries for a topic.

    Falls back to a simple heuristic if Ollama is unavailable.
    """
    prompt = textwrap.dedent(f"""\
        You are a research assistant. Generate exactly {n} targeted, specific web search
        queries to thoroughly research the following topic.

        TOPIC: {topic}

        RESEARCH CONTEXT:
        {template_context[:1000]}

        Rules:
        - One query per line, no numbering, no bullet points.
        - Vary the angle (definition, comparison, implementation, alternatives, pitfalls).
        - Prefer exact technical terms, tool names, and version numbers where relevant.
        - Output ONLY the queries, nothing else.
    """)

    queries = await _ollama_complete(prompt, max_tokens=512)

    if not queries:
        # Minimal fallback
        return [
            f"{topic} overview",
            f"{topic} tutorial",
            f"{topic} best practices",
            f"{topic} alternatives",
            f"{topic} 2025",
        ]

    lines = [ln.strip() for ln in queries.splitlines() if ln.strip()]
    return lines[:n] if len(lines) >= n else lines


# ---------------------------------------------------------------------------
# Search (Step 3)
# ---------------------------------------------------------------------------


async def _execute_search(queries: list[str]) -> list[dict[str, str]]:
    """Run each query through the available web search backend.

    Returns a flat list of {title, url, snippet} dicts.
    Degrades gracefully if SerpAPI key is absent.
    """
    results: list[dict[str, str]] = []
    seen_urls: set[str] = set()

    for query in queries:
        try:
            raw = await asyncio.to_thread(_run_search_sync, query)
            for item in raw:
                url = item.get("url", "")
                if url and url not in seen_urls:
                    seen_urls.add(url)
                    results.append(item)
        except Exception as exc:
            logger.warning("Search failed for query %r: %s", query, exc)

    return results


def _run_search_sync(query: str) -> list[dict[str, str]]:
    """Synchronous search — wraps SerpAPI or returns empty on missing key."""
    import os

    if not os.environ.get("SERPAPI_API_KEY"):
        logger.debug("SERPAPI_API_KEY not set — skipping web search for %r", query)
        return []

    try:
        from serpapi import GoogleSearch

        params = {"q": query, "api_key": os.environ["SERPAPI_API_KEY"], "num": 5}
        search = GoogleSearch(params)
        data = search.get_dict()
        items = []
        for r in data.get("organic_results", []):
            items.append(
                {
                    "title": r.get("title", ""),
                    "url": r.get("link", ""),
                    "snippet": r.get("snippet", ""),
                }
            )
        return items
    except Exception as exc:
        logger.warning("SerpAPI search error: %s", exc)
        return []


# ---------------------------------------------------------------------------
# Fetch (Step 4)
# ---------------------------------------------------------------------------


async def _fetch_pages(results: list[dict[str, str]], top_n: int = _FETCH_TOP_N) -> list[str]:
    """Download and extract full text for the top search results.

    Uses web_fetch (trafilatura) from timmy.tools.system_tools.
    """
    try:
        from timmy.tools.system_tools import web_fetch
    except ImportError:
        logger.warning("web_fetch not available — skipping page fetch")
        return []

    pages: list[str] = []
    for item in results[:top_n]:
        url = item.get("url", "")
        if not url:
            continue
        try:
            text = await asyncio.to_thread(web_fetch, url, 6000)
            if text and not text.startswith("Error:"):
                pages.append(f"## {item.get('title', url)}\nSource: {url}\n\n{text}")
        except Exception as exc:
            logger.warning("Failed to fetch %s: %s", url, exc)

    return pages


# ---------------------------------------------------------------------------
# Synthesis (Step 5) — cascade: Ollama → Claude fallback
# ---------------------------------------------------------------------------


async def _synthesize(topic: str, pages: list[str], snippets: list[str]) -> tuple[str, str]:
    """Compress fetched pages + snippets into a structured research report.

    Returns (report_markdown, backend_used).
    """
    # Build synthesis prompt
    source_content = "\n\n---\n\n".join(pages[:5])
    if not source_content and snippets:
        source_content = "\n".join(f"- {s}" for s in snippets[:20])

    if not source_content:
        return (
            f"# Research: {topic}\n\n*No source material was retrieved. "
            "Check SERPAPI_API_KEY and network connectivity.*",
            "none",
        )

    prompt = textwrap.dedent(f"""\
        You are a senior technical researcher. Synthesize the source material below
        into a structured research report on the topic: **{topic}**

        FORMAT YOUR REPORT AS:
        # {topic}

        ## Executive Summary
        (2-3 sentences: what you found, top recommendation)

        ## Key Findings
        (Bullet list of the most important facts, tools, or patterns)

        ## Comparison / Options
        (Table or list comparing alternatives where applicable)

        ## Recommended Approach
        (Concrete recommendation with rationale)

        ## Gaps & Next Steps
        (What wasn't answered, what to investigate next)

        ---
        SOURCE MATERIAL:
        {source_content[:12000]}
    """)

    # Tier 3 — try Ollama first
    report = await _ollama_complete(prompt, max_tokens=_SYNTHESIS_MAX_TOKENS)
    if report:
        return report, "ollama"

    # Tier 2 — Claude fallback
    report = await _claude_complete(prompt, max_tokens=_SYNTHESIS_MAX_TOKENS)
    if report:
        return report, "claude"

    # Last resort — structured snippet summary
    summary = f"# {topic}\n\n## Snippets\n\n" + "\n\n".join(f"- {s}" for s in snippets[:15])
    return summary, "fallback"


# ---------------------------------------------------------------------------
# LLM helpers
# ---------------------------------------------------------------------------


async def _ollama_complete(prompt: str, max_tokens: int = 1024) -> str:
    """Send a prompt to Ollama and return the response text.

    Returns empty string on failure (graceful degradation).
    """
    try:
        import httpx

        from config import settings

        url = f"{settings.normalized_ollama_url}/api/generate"
        payload: dict[str, Any] = {
            "model": settings.ollama_model,
            "prompt": prompt,
            "stream": False,
            "options": {
                "num_predict": max_tokens,
                "temperature": 0.3,
            },
        }

        async with httpx.AsyncClient(timeout=120.0) as client:
            resp = await client.post(url, json=payload)
            resp.raise_for_status()
            data = resp.json()
            return data.get("response", "").strip()
    except Exception as exc:
        logger.warning("Ollama completion failed: %s", exc)
        return ""


async def _claude_complete(prompt: str, max_tokens: int = 1024) -> str:
    """Send a prompt to Claude API as a last-resort fallback.

    Only active when ANTHROPIC_API_KEY is configured.
    Returns empty string on failure or missing key.
    """
    try:
        from config import settings

        if not settings.anthropic_api_key:
            return ""

        from timmy.backends import ClaudeBackend

        backend = ClaudeBackend()
        result = await asyncio.to_thread(backend.run, prompt)
        return result.content.strip()
    except Exception as exc:
        logger.warning("Claude fallback failed: %s", exc)
        return ""
