"""
mcp_server/engine/synthesizer.py — Cross-lens synthesis, comparison, deep dive.

Higher-order LLM operations that operate on *existing* LensAnalysis results
rather than raw text.  Uses the MCP host LLM via sampling.
"""

from __future__ import annotations

import json
import logging
from typing import Any

from mcp_server.engine.lens_runner import SamplingFn
from mcp_server.models import (
    DeepDiveResult,
    Finding,
    LensAnalysis,
    LensConnection,
    LensName,
    Synthesis,
    TextComparison,
)
from mcp_server.prompts.vocabulary import (
    COMPARISON_PROMPT,
    DEEP_DIVE_PROMPT,
    SYNTHESIS_PROMPT,
)

logger = logging.getLogger(__name__)


# ── Synthesis ──────────────────────────────────────────────────────────

async def run_synthesis(
    sample: SamplingFn,
    text_id: str,
    analyses: list[LensAnalysis],
) -> Synthesis:
    """Synthesise multiple lens analyses into a unified interpretation.

    Args:
        sample: Async callable (system_prompt, user_prompt) -> str.
        text_id: The text these analyses belong to.
        analyses: List of completed LensAnalysis objects (one per lens).
    """
    lens_names = ", ".join(a.lens.value for a in analyses)
    analyses_text = _format_analyses_for_prompt(analyses)

    system_prompt = SYNTHESIS_PROMPT.format(
        lens_count=len(analyses),
        lens_names=lens_names,
        analyses_text=analyses_text,
    )

    user_prompt = f"""\
Synthesise the above analyses for text **{text_id}**.

Respond in JSON with this exact schema:
{{
  "unified_interpretation": "...",
  "cross_connections": [
    {{
      "from_lens": "lens_name",
      "to_lens": "lens_name",
      "relationship": "...",
      "strength": 0.0-1.0
    }}
  ],
  "emergent_insights": ["insight1", "insight2", ...],
  "overall_confidence": 0.0-1.0
}}"""

    raw = await sample(system_prompt, user_prompt)
    return _parse_synthesis(raw, text_id, analyses)


# ── Comparison ─────────────────────────────────────────────────────────

async def run_comparison(
    sample: SamplingFn,
    analyses_by_text: dict[str, LensAnalysis],
    lens: LensName,
) -> TextComparison:
    """Compare analyses of different texts through the same lens.

    Args:
        sample: Async callable (system_prompt, user_prompt) -> str.
        analyses_by_text: {text_id: LensAnalysis} for several texts, all same lens.
        lens: The lens being compared.
    """
    analyses_text = "\n\n".join(
        f"### Text: {tid}\n{a.raw_prose}" for tid, a in analyses_by_text.items()
    )
    system_prompt = COMPARISON_PROMPT.format(
        lens_name=lens.value,
        analyses_text=analyses_text,
    )

    user_prompt = f"""\
Compare the {lens.value} analyses of texts: {', '.join(analyses_by_text.keys())}.

Respond in JSON:
{{
  "shared_patterns": ["pattern1", ...],
  "divergences": ["divergence1", ...],
  "comparative_insight": "..."
}}"""

    raw = await sample(system_prompt, user_prompt)
    return _parse_comparison(raw, list(analyses_by_text.keys()), lens)


# ── Deep Dive ──────────────────────────────────────────────────────────

async def run_deep_dive(
    sample: SamplingFn,
    text: str,
    text_id: str,
    lens: LensName,
    finding_text: str,
) -> DeepDiveResult:
    """Drill deeper into a specific finding from an analysis.

    Args:
        sample: Async callable (system_prompt, user_prompt) -> str.
        text: The original source text (for re-reading context).
        text_id: Text identifier.
        lens: Which lens the finding came from.
        finding_text: The original finding description to explore.
    """
    system_prompt = DEEP_DIVE_PROMPT.format(
        lens_name=lens.value,
        finding_text=finding_text,
        text_id=text_id,
    )

    user_prompt = f"""\
Here is the original text for context:
---
{text}
---

Drill deeper into the finding described above.

Respond in JSON:
{{
  "extended_analysis": "...",
  "sub_findings": [
    {{
      "claim": "...",
      "evidence": "...",
      "formal_notation": "... or null",
      "confidence": 0.0-1.0
    }}
  ],
  "related_connections": [
    {{
      "from_lens": "{lens.value}",
      "to_lens": "other_lens",
      "relationship": "...",
      "strength": 0.0-1.0
    }}
  ]
}}"""

    raw = await sample(system_prompt, user_prompt)
    return _parse_deep_dive(raw, text_id, lens, finding_text)


# ── Private helpers ────────────────────────────────────────────────────

def _format_analyses_for_prompt(analyses: list[LensAnalysis]) -> str:
    """Render completed analyses into a readable prompt section."""
    parts = []
    for a in analyses:
        findings_str = "\n".join(
            f"  - {f.claim} (confidence: {f.confidence})" for f in a.findings
        )
        parts.append(
            f"### {a.lens.value} Lens\n"
            f"**Summary**: {a.summary}\n"
            f"**Findings**:\n{findings_str}\n"
            f"**Vocabulary used**: {', '.join(a.vocabulary_used)}"
        )
    return "\n\n".join(parts)


def _parse_synthesis(
    raw_json: str, text_id: str, analyses: list[LensAnalysis], model: str = "host"
) -> Synthesis:
    data: dict[str, Any] = json.loads(raw_json)

    connections = [
        LensConnection(
            from_lens=_coerce_lens(c.get("from_lens", "")),
            to_lens=_coerce_lens(c.get("to_lens", "")),
            relationship=c.get("relationship", ""),
            strength=_clamp(c.get("strength", 0.5)),
        )
        for c in data.get("cross_connections", [])
    ]

    return Synthesis(
        text_id=text_id,
        lenses_included=[a.lens for a in analyses],
        unified_interpretation=data.get("unified_interpretation", ""),
        cross_connections=connections,
        emergent_insights=data.get("emergent_insights", []),
        overall_confidence=_clamp(data.get("overall_confidence", 0.5)),
        model=model,
    )


def _parse_comparison(
    raw_json: str, text_ids: list[str], lens: LensName, model: str = "host"
) -> TextComparison:
    data: dict[str, Any] = json.loads(raw_json)

    return TextComparison(
        text_ids=text_ids,
        lens=lens,
        shared_patterns=data.get("shared_patterns", []),
        divergences=data.get("divergences", []),
        comparative_insight=data.get("comparative_insight", ""),
        model=model,
    )


def _parse_deep_dive(
    raw_json: str, text_id: str, lens: LensName,
    finding_text: str, model: str = "host",
) -> DeepDiveResult:
    data: dict[str, Any] = json.loads(raw_json)

    sub_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("sub_findings", [])
    ]

    connections = [
        LensConnection(
            from_lens=_coerce_lens(c.get("from_lens", "")),
            to_lens=_coerce_lens(c.get("to_lens", "")),
            relationship=c.get("relationship", ""),
            strength=_clamp(c.get("strength", 0.5)),
        )
        for c in data.get("related_connections", [])
    ]

    return DeepDiveResult(
        text_id=text_id,
        lens=lens,
        original_finding=finding_text,
        extended_analysis=data.get("extended_analysis", ""),
        sub_findings=sub_findings,
        related_connections=connections,
        model=model,
    )


def _coerce_lens(value: str) -> LensName:
    """Best-effort coerce a string to LensName, falling back to ONTOLOGICAL."""
    cleaned = value.strip().lower().replace("-", "_").replace(" ", "_")
    for member in LensName:
        if member.value == cleaned:
            return member
    # Fuzzy fallback: check if the value starts with a known prefix
    for member in LensName:
        if cleaned.startswith(member.value[:4]):
            return member
    return LensName.ONTOLOGICAL


def _clamp(value: Any, lo: float = 0.0, hi: float = 1.0) -> float:
    try:
        return max(lo, min(hi, float(value)))
    except (TypeError, ValueError):
        return 0.5
