"""
mcp_server/engine/lens_runner.py — Core async analysis engine.

Takes a text + LensName, constructs the vocabulary prompt, calls the MCP
host's LLM via sampling, and returns a validated LensAnalysis.
"""

from __future__ import annotations

import json
import logging
from typing import Any, Callable, Awaitable

from mcp_server.models import Finding, LensAnalysis, LensName
from mcp_server.prompts.vocabulary import LENS_PROMPTS

logger = logging.getLogger(__name__)

# Type alias: an async callable (system_prompt, user_prompt) -> str
SamplingFn = Callable[[str, str], Awaitable[str]]


async def run_lens_analysis(
    sample: SamplingFn,
    text: str,
    text_id: str,
    lens: LensName,
) -> LensAnalysis:
    """Run a single-lens analysis on a text via the host LLM.

    Args:
        sample: Async callable that sends (system_prompt, user_prompt)
                to the MCP host and returns the response text.
        text: The full source text to analyse.
        text_id: Identifier for the text.
        lens: Which analytical lens to apply.

    Returns:
        A validated LensAnalysis Pydantic model.
    """
    system_prompt = LENS_PROMPTS[lens]
    user_prompt = _build_user_prompt(text, text_id, lens)

    raw = await sample(system_prompt, user_prompt)
    logger.debug("LLM raw response for %s/%s: %s", text_id, lens.value, raw[:200])

    return _parse_analysis(raw, text_id, lens)


def _build_user_prompt(text: str, text_id: str, lens: LensName) -> str:
    """Construct the user message for analysis."""
    return f"""\
Analyse the following text through the **{lens.value}** lens.

**Text ID**: {text_id}
**Text**:
---
{text}
---

Respond in JSON with this exact schema:
{{
  "summary": "2-3 sentence overview",
  "findings": [
    {{
      "claim": "...",
      "evidence": "...",
      "formal_notation": "... or null",
      "confidence": 0.0-1.0
    }}
  ],
  "vocabulary_used": ["term1", "term2", ...],
  "formal_section": "extended formal notation or null",
  "raw_prose": "full narrative analysis"
}}"""


def _parse_analysis(
    raw_json: str, text_id: str, lens: LensName, model: str = "host"
) -> LensAnalysis:
    """Parse LLM JSON response into a validated LensAnalysis."""
    data: dict[str, Any] = json.loads(raw_json)

    findings = [
        Finding(
            claim=f.get("claim", ""),
            evidence=f.get("evidence", ""),
            formal_notation=f.get("formal_notation"),
            confidence=_clamp(f.get("confidence", 0.5)),
        )
        for f in data.get("findings", [])
    ]

    return LensAnalysis(
        text_id=text_id,
        lens=lens,
        summary=data.get("summary", ""),
        findings=findings,
        vocabulary_used=data.get("vocabulary_used", []),
        formal_section=data.get("formal_section"),
        raw_prose=data.get("raw_prose", ""),
        model=model,
    )


def _clamp(value: Any, lo: float = 0.0, hi: float = 1.0) -> float:
    """Clamp a numeric value to [lo, hi], defaulting to 0.5 for non-numeric."""
    try:
        return max(lo, min(hi, float(value)))
    except (TypeError, ValueError):
        return 0.5
