"""Research coordinator — orchestrator, data structures, cache, and disk I/O.

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

from __future__ import annotations

import logging
import re
from dataclasses import dataclass, field
from pathlib import Path

logger = logging.getLogger(__name__)

# Optional memory imports — available at module level so tests can patch them.
try:
    from timmy.memory_system import SemanticMemory, store_memory
except Exception:  # pragma: no cover
    SemanticMemory = None  # type: ignore[assignment,misc]
    store_memory = None  # type: ignore[assignment]

# Root of the project — two levels up from src/timmy/research/
_PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
_SKILLS_ROOT = _PROJECT_ROOT / "skills" / "research"
_DOCS_ROOT = _PROJECT_ROOT / "docs" / "research"

# Similarity threshold for cache hit (0–1 cosine similarity)
_CACHE_HIT_THRESHOLD = 0.82

# How many search result URLs to fetch as full pages
_FETCH_TOP_N = 5

# Maximum tokens to request from the synthesis LLM
_SYNTHESIS_MAX_TOKENS = 4096


# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------


@dataclass
class ResearchResult:
    """Full output of a research pipeline run."""

    topic: str
    query_count: int
    sources_fetched: int
    report: str
    cached: bool = False
    cache_similarity: float = 0.0
    synthesis_backend: str = "unknown"
    errors: list[str] = field(default_factory=list)

    def is_empty(self) -> bool:
        return not self.report.strip()


# ---------------------------------------------------------------------------
# Template loading
# ---------------------------------------------------------------------------


def list_templates() -> list[str]:
    """Return names of available research templates (without .md extension)."""
    if not _SKILLS_ROOT.exists():
        return []
    return [p.stem for p in sorted(_SKILLS_ROOT.glob("*.md"))]


def load_template(template_name: str, slots: dict[str, str] | None = None) -> str:
    """Load a research template and fill {slot} placeholders.

    Args:
        template_name: Stem of the .md file under skills/research/ (e.g. "tool_evaluation").
        slots: Mapping of {placeholder} → replacement value.

    Returns:
        Template text with slots filled. Unfilled slots are left as-is.
    """
    path = _SKILLS_ROOT / f"{template_name}.md"
    if not path.exists():
        available = ", ".join(list_templates()) or "(none)"
        raise FileNotFoundError(
            f"Research template {template_name!r} not found. Available: {available}"
        )

    text = path.read_text(encoding="utf-8")

    # Strip YAML frontmatter (--- ... ---), including empty frontmatter (--- \n---)
    text = re.sub(r"^---\n.*?---\n", "", text, flags=re.DOTALL)

    if slots:
        for key, value in slots.items():
            text = text.replace(f"{{{key}}}", value)

    return text.strip()


# ---------------------------------------------------------------------------
# Memory cache (Step 0 + Step 6)
# ---------------------------------------------------------------------------


def _check_cache(topic: str) -> tuple[str | None, float]:
    """Search semantic memory for a prior result on this topic.

    Returns (cached_report, similarity) or (None, 0.0).
    """
    try:
        if SemanticMemory is None:
            return None, 0.0
        mem = SemanticMemory()
        hits = mem.search(topic, top_k=1)
        if hits:
            content, score = hits[0]
            if score >= _CACHE_HIT_THRESHOLD:
                return content, score
    except Exception as exc:
        logger.debug("Cache check failed: %s", exc)
    return None, 0.0


def _store_result(topic: str, report: str) -> None:
    """Index the research report into semantic memory for future retrieval."""
    try:
        if store_memory is None:
            logger.debug("store_memory not available — skipping memory index")
            return
        store_memory(
            content=report,
            source="research_pipeline",
            context_type="research",
            metadata={"topic": topic},
        )
        logger.info("Research result indexed for topic: %r", topic)
    except Exception as exc:
        logger.warning("Failed to store research result: %s", exc)


def _save_to_disk(topic: str, report: str) -> Path | None:
    """Persist the report as a markdown file under docs/research/.

    Filename is derived from the topic (slugified). Returns the path or None.
    """
    try:
        slug = re.sub(r"[^a-z0-9]+", "-", topic.lower()).strip("-")[:60]
        _DOCS_ROOT.mkdir(parents=True, exist_ok=True)
        path = _DOCS_ROOT / f"{slug}.md"
        path.write_text(report, encoding="utf-8")
        logger.info("Research report saved to %s", path)
        return path
    except Exception as exc:
        logger.warning("Failed to save research report to disk: %s", exc)
        return None


# ---------------------------------------------------------------------------
# Main orchestrator
# ---------------------------------------------------------------------------


async def run_research(
    topic: str,
    template: str | None = None,
    slots: dict[str, str] | None = None,
    save_to_disk: bool = False,
    skip_cache: bool = False,
) -> ResearchResult:
    """Run the full 6-step autonomous research pipeline.

    Args:
        topic:        The research question or subject.
        template:     Name of a template from skills/research/ (e.g. "tool_evaluation").
                      If None, runs without a template scaffold.
        slots:        Placeholder values for the template (e.g. {"domain": "PDF parsing"}).
        save_to_disk: If True, write the report to docs/research/<slug>.md.
        skip_cache:   If True, bypass the semantic memory cache.

    Returns:
        ResearchResult with report and metadata.
    """
    from timmy.research.sources import (
        _execute_search,
        _fetch_pages,
        _formulate_queries,
        _synthesize,
    )

    errors: list[str] = []

    # ------------------------------------------------------------------
    # Step 0 — check cache
    # ------------------------------------------------------------------
    if not skip_cache:
        cached, score = _check_cache(topic)
        if cached:
            logger.info("Cache hit (%.2f) for topic: %r", score, topic)
            return ResearchResult(
                topic=topic,
                query_count=0,
                sources_fetched=0,
                report=cached,
                cached=True,
                cache_similarity=score,
                synthesis_backend="cache",
            )

    # ------------------------------------------------------------------
    # Step 1 — load template (optional)
    # ------------------------------------------------------------------
    template_context = ""
    if template:
        try:
            template_context = load_template(template, slots)
        except FileNotFoundError as exc:
            errors.append(str(exc))
            logger.warning("Template load failed: %s", exc)

    # ------------------------------------------------------------------
    # Step 2 — formulate queries
    # ------------------------------------------------------------------
    queries = await _formulate_queries(topic, template_context)
    logger.info("Formulated %d queries for topic: %r", len(queries), topic)

    # ------------------------------------------------------------------
    # Step 3 — execute search
    # ------------------------------------------------------------------
    search_results = await _execute_search(queries)
    logger.info("Search returned %d results", len(search_results))
    snippets = [r.get("snippet", "") for r in search_results if r.get("snippet")]

    # ------------------------------------------------------------------
    # Step 4 — fetch full pages
    # ------------------------------------------------------------------
    pages = await _fetch_pages(search_results)
    logger.info("Fetched %d pages", len(pages))

    # ------------------------------------------------------------------
    # Step 5 — synthesize
    # ------------------------------------------------------------------
    report, backend = await _synthesize(topic, pages, snippets)

    # ------------------------------------------------------------------
    # Step 6 — deliver
    # ------------------------------------------------------------------
    _store_result(topic, report)
    if save_to_disk:
        _save_to_disk(topic, report)

    return ResearchResult(
        topic=topic,
        query_count=len(queries),
        sources_fetched=len(pages),
        report=report,
        cached=False,
        synthesis_backend=backend,
        errors=errors,
    )
