"""
mcp_server/server.py — FastMCP server for 7-lens theological text analysis.

This is the main server file that wires together:
  - Lifespan: SQLite init, text loading
  - Tools: analyze_with_lens, synthesize, compare_texts, deep_dive, search_cache, export_report
  - Resources: dusun://texts, dusun://texts/{id}, dusun://glossary/{lens}, dusun://analysis/{id}/{lens}
  - Prompts: full_7lens_analysis, comparative_analysis, deep_exploration

Design: One analyze_with_lens tool (not 7 separate) saves tool budget.
Structured output via Pydantic models auto-serialised by FastMCP.

LLM calls go through the MCP host via sampling — no separate API key needed.
"""

from __future__ import annotations

import json
import logging
import os
import sqlite3
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import Optional

from mcp.server.fastmcp import Context, FastMCP
from mcp.types import SamplingMessage, TextContent

from mcp_server.engine.lens_runner import run_lens_analysis
from mcp_server.engine.synthesizer import (
    run_comparison,
    run_deep_dive,
    run_synthesis,
)
from mcp_server.models import (
    AnalysisStatus,
    LensAnalysis,
    LensName,
    TextInfo,
)
from mcp_server.persistence.db import (
    count_cached_analyses,
    init_db,
    list_analyses_for_text,
    load_all_analyses_for_text,
    load_analysis,
    load_synthesis,
    save_analysis,
    save_synthesis,
    search_analyses,
)
from mcp_server.prompts.vocabulary import LENS_PROMPTS

logger = logging.getLogger("mcp_server")


# ── Lifespan state shared across all tool/resource calls ────────────

@dataclass
class AppState:
    """Holds shared state initialised during lifespan."""
    db: sqlite3.Connection
    texts: dict[str, TextInfo]        # text_id → TextInfo
    text_contents: dict[str, str]     # text_id → full text string


def _load_source_texts(base_dir: Path) -> tuple[dict[str, TextInfo], dict[str, str]]:
    """Load source texts from kaynak_metinler/ directory."""
    texts: dict[str, TextInfo] = {}
    contents: dict[str, str] = {}

    kaynak = base_dir / "kaynak_metinler"
    if not kaynak.is_dir():
        logger.warning("kaynak_metinler/ not found at %s", kaynak)
        return texts, contents

    for child in sorted(kaynak.iterdir()):
        if not child.is_dir() or child.name.startswith("_"):
            continue
        md_file = child / "original.md"
        if not md_file.exists():
            continue
        content = md_file.read_text(encoding="utf-8")
        text_id = child.name
        word_count = len(content.split())
        preview = content[:200].replace("\n", " ")
        texts[text_id] = TextInfo(
            text_id=text_id,
            title=text_id,
            word_count=word_count,
            preview=preview,
        )
        contents[text_id] = content
        logger.info("Loaded text: %s (%d words)", text_id, word_count)

    # Also load from evaluation corpus if present
    corpus_path = base_dir / "evaluation" / "corpus.json"
    if corpus_path.exists():
        try:
            corpus = json.loads(corpus_path.read_text(encoding="utf-8"))
            for t in corpus.get("texts", []):
                tid = t.get("id", "")
                if tid and tid not in texts:
                    text = t.get("text", "")
                    texts[tid] = TextInfo(
                        text_id=tid,
                        title=t.get("source", tid),
                        word_count=len(text.split()),
                        preview=text[:200].replace("\n", " "),
                    )
                    contents[tid] = text
            logger.info("Loaded %d additional texts from corpus", len(corpus.get("texts", [])))
        except Exception:
            logger.debug("Could not load evaluation corpus", exc_info=True)

    return texts, contents


@asynccontextmanager
async def _lifespan(server: FastMCP) -> AsyncIterator[AppState]:
    """Initialise SQLite, OpenAI client, load texts; yield AppState."""
    # Database path (configurable via env)
    db_path = os.environ.get(
        "DUSUN_DB_PATH",
        str(Path.cwd() / ".dusun_cache" / "analyses.db"),
    )
    db = init_db(db_path)
    logger.info("SQLite database at %s", db_path)

    # Load source texts
    project_root = Path(os.environ.get("DUSUN_PROJECT_ROOT", str(Path.cwd())))
    texts, contents = _load_source_texts(project_root)
    logger.info("Loaded %d source texts", len(texts))

    state = AppState(
        db=db,
        texts=texts,
        text_contents=contents,
    )

    try:
        yield state
    finally:
        db.close()
        logger.info("Database closed")


# ── FastMCP instance ──────────────────────────────────────────────────

mcp = FastMCP(
    "dusun-analysis",
    instructions=(
        "7-lens theological text analysis server. Use analyze_with_lens to "
        "apply ontological, mereological, FOL, Bayesian, game-theoretic, "
        "category-theoretic, or topological lenses to source texts. Then "
        "synthesize cross-lens insights or compare across texts."
    ),
    lifespan=_lifespan,
)


# ── Helpers ──────────────────────────────────────────────────────────

def _state(ctx: Context) -> AppState:
    """Extract AppState from the MCP context."""
    return ctx.request_context.lifespan_context


async def _sample(ctx: Context, system_prompt: str, user_prompt: str) -> str:
    """Ask the MCP host to run an LLM completion (sampling).

    This uses the model the user is already paying for (e.g. Copilot/Claude)
    instead of requiring a separate API key.
    """
    result = await ctx.session.create_message(
        messages=[
            SamplingMessage(
                role="user",
                content=TextContent(type="text", text=user_prompt),
            ),
        ],
        system_prompt=system_prompt,
        max_tokens=4096,
        include_context="thisServer",
    )
    # result.content is TextContent | ImageContent | AudioContent
    if hasattr(result.content, "text"):
        return result.content.text
    return str(result.content)


# =====================================================================
# TOOL 1: analyze_with_lens
# =====================================================================

@mcp.tool()
async def analyze_with_lens(
    text_id: str,
    lens: str,
    force: bool = False,
    ctx: Context = None,
) -> str:
    """Apply a single analytical lens to a text. Returns structured analysis.

    Args:
        text_id: ID of the text to analyse (e.g. "birinciSoz").
        lens: Lens name — one of: ontological, mereological, fol, bayesian,
              game_theoretic, category_theoretic, topological.
        force: If True, re-analyse even if cached. Default False.
    """
    state = _state(ctx)
    lens_enum = LensName(lens)

    # Validate text exists
    if text_id not in state.text_contents:
        available = ", ".join(sorted(state.texts.keys()))
        return f"Unknown text_id '{text_id}'. Available: {available}"

    # Check cache first
    if not force:
        cached = load_analysis(state.db, text_id, lens_enum.value)
        if cached:
            analysis = LensAnalysis.model_validate_json(cached)
            return (
                f"**{lens_enum.value} analysis of {text_id}** (cached)\n\n"
                f"{analysis.summary}\n\n"
                f"Findings: {len(analysis.findings)} | "
                f"Vocabulary: {len(analysis.vocabulary_used)} terms\n\n"
                f"```json\n{cached}\n```"
            )

    # Run fresh analysis
    await ctx.report_progress(0, 1, f"Analysing {text_id} with {lens_enum.value} lens...")
    text = state.text_contents[text_id]

    async def sample(sys: str, usr: str) -> str:
        return await _sample(ctx, sys, usr)

    analysis = await run_lens_analysis(sample, text, text_id, lens_enum)

    # Persist result
    result_json = analysis.model_dump_json(indent=2)
    save_analysis(state.db, text_id, lens_enum.value, "host", result_json)
    await ctx.report_progress(1, 1)

    return (
        f"**{lens_enum.value} analysis of {text_id}** (fresh)\n\n"
        f"{analysis.summary}\n\n"
        f"Findings: {len(analysis.findings)} | "
        f"Vocabulary: {len(analysis.vocabulary_used)} terms\n\n"
        f"```json\n{result_json}\n```"
    )


# =====================================================================
# TOOL 2: synthesize_analyses
# =====================================================================

@mcp.tool()
async def synthesize_analyses(
    text_id: str,
    force: bool = False,
    ctx: Context = None,
) -> str:
    """Synthesise all completed lens analyses for a text into unified insights.

    Requires at least 2 completed lens analyses for the text.
    Run analyze_with_lens first for each desired lens.

    Args:
        text_id: ID of the text to synthesise.
        force: If True, re-synthesise even if cached. Default False.
    """
    state = _state(ctx)

    # Check cache
    if not force:
        cached = load_synthesis(state.db, text_id)
        if cached:
            return f"**Synthesis for {text_id}** (cached)\n\n```json\n{cached}\n```"

    # Load all lens analyses
    all_raw = load_all_analyses_for_text(state.db, text_id)
    if len(all_raw) < 2:
        done = list(all_raw.keys()) if all_raw else []
        return (
            f"Need at least 2 completed analyses to synthesise. "
            f"Currently done for {text_id}: {done}. "
            f"Run analyze_with_lens first."
        )

    analyses = [
        LensAnalysis.model_validate_json(rj) for rj in all_raw.values()
    ]

    await ctx.report_progress(0, 1, "Synthesising cross-lens insights...")

    async def sample(sys: str, usr: str) -> str:
        return await _sample(ctx, sys, usr)

    synthesis = await run_synthesis(sample, text_id, analyses)

    result_json = synthesis.model_dump_json(indent=2)
    lenses = [a.lens.value for a in analyses]
    save_synthesis(state.db, text_id, lenses, "host", result_json)
    await ctx.report_progress(1, 1)

    return (
        f"**Synthesis for {text_id}** ({len(analyses)} lenses)\n\n"
        f"Cross-connections: {len(synthesis.cross_connections)} | "
        f"Emergent insights: {len(synthesis.emergent_insights)}\n\n"
        f"```json\n{result_json}\n```"
    )


# =====================================================================
# TOOL 3: compare_texts
# =====================================================================

@mcp.tool()
async def compare_texts(
    text_ids: list[str],
    lens: str,
    ctx: Context = None,
) -> str:
    """Compare multiple texts through a single lens.

    All specified texts must already have completed analyses for the given lens.

    Args:
        text_ids: List of text IDs to compare (at least 2).
        lens: Which lens to compare through.
    """
    state = _state(ctx)
    lens_enum = LensName(lens)

    if len(text_ids) < 2:
        return "Need at least 2 text_ids for comparison."

    # Load analyses for each text
    analyses_by_text = {}
    missing = []
    for tid in text_ids:
        raw = load_analysis(state.db, tid, lens_enum.value)
        if raw:
            analyses_by_text[tid] = LensAnalysis.model_validate_json(raw)
        else:
            missing.append(tid)

    if missing:
        return (
            f"Missing {lens_enum.value} analyses for: {missing}. "
            f"Run analyze_with_lens first."
        )

    await ctx.report_progress(0, 1, f"Comparing {len(text_ids)} texts through {lens_enum.value}...")

    async def sample(sys: str, usr: str) -> str:
        return await _sample(ctx, sys, usr)

    comparison = await run_comparison(sample, analyses_by_text, lens_enum)
    await ctx.report_progress(1, 1)

    result_json = comparison.model_dump_json(indent=2)
    return (
        f"**{lens_enum.value} comparison** of {', '.join(text_ids)}\n\n"
        f"Shared patterns: {len(comparison.shared_patterns)} | "
        f"Divergences: {len(comparison.divergences)}\n\n"
        f"```json\n{result_json}\n```"
    )


# =====================================================================
# TOOL 4: deep_dive
# =====================================================================

@mcp.tool()
async def deep_dive(
    text_id: str,
    lens: str,
    finding_index: int = 0,
    ctx: Context = None,
) -> str:
    """Drill deeper into a specific finding from a lens analysis.

    Args:
        text_id: ID of the text.
        lens: The lens the finding came from.
        finding_index: Index of the finding to explore (0-based). Default 0.
    """
    state = _state(ctx)
    lens_enum = LensName(lens)

    # Load the existing analysis
    raw = load_analysis(state.db, text_id, lens_enum.value)
    if not raw:
        return f"No {lens_enum.value} analysis found for {text_id}. Run analyze_with_lens first."

    analysis = LensAnalysis.model_validate_json(raw)
    if finding_index >= len(analysis.findings):
        return (
            f"Finding index {finding_index} out of range. "
            f"Analysis has {len(analysis.findings)} findings (0-{len(analysis.findings) - 1})."
        )

    finding = analysis.findings[finding_index]
    finding_text = f"{finding.claim}\nEvidence: {finding.evidence}"

    if text_id not in state.text_contents:
        return f"Source text '{text_id}' not available for deep dive."

    await ctx.report_progress(0, 1, f"Deep diving into finding {finding_index}...")

    async def sample(sys: str, usr: str) -> str:
        return await _sample(ctx, sys, usr)

    result = await run_deep_dive(
        sample,
        state.text_contents[text_id],
        text_id,
        lens_enum,
        finding_text,
    )
    await ctx.report_progress(1, 1)

    result_json = result.model_dump_json(indent=2)
    return (
        f"**Deep dive**: {text_id} / {lens_enum.value} / finding #{finding_index}\n\n"
        f"{result.extended_analysis[:300]}...\n\n"
        f"Sub-findings: {len(result.sub_findings)} | "
        f"Cross-connections: {len(result.related_connections)}\n\n"
        f"```json\n{result_json}\n```"
    )


# =====================================================================
# TOOL 5: search_cache
# =====================================================================

@mcp.tool()
async def search_cache(
    query: str,
    lens: Optional[str] = None,
    ctx: Context = None,
) -> str:
    """Search all cached analyses for a keyword or concept.

    Args:
        query: Search term to look for in analysis results.
        lens: Optionally filter by lens name.
    """
    state = _state(ctx)
    results = search_analyses(state.db, query, lens)
    if not results:
        return f"No cached analyses matching '{query}'."

    lines = [f"**Search results for '{query}'** ({len(results)} matches)\n"]
    for r in results:
        lines.append(f"- **{r['text_id']}** / {r['lens']}: {r['snippet']}")
    return "\n".join(lines)


# =====================================================================
# TOOL 6: get_analysis_status
# =====================================================================

@mcp.tool()
async def get_analysis_status(
    text_id: str,
    ctx: Context = None,
) -> str:
    """Check which lens analyses are completed for a text.

    Args:
        text_id: ID of the text to check.
    """
    state = _state(ctx)
    all_lenses = set(ln.value for ln in LensName)
    done_lenses = set(list_analyses_for_text(state.db, text_id))
    pending = all_lenses - done_lenses
    has_synth = load_synthesis(state.db, text_id) is not None
    total_cached = count_cached_analyses(state.db)

    status = AnalysisStatus(
        text_id=text_id,
        completed_lenses=[LensName(l) for l in sorted(done_lenses)],
        pending_lenses=[LensName(l) for l in sorted(pending)],
        has_synthesis=has_synth,
        cached_count=total_cached,
    )
    return (
        f"**Status for {text_id}**\n"
        f"Completed: {[l.value for l in status.completed_lenses]}\n"
        f"Pending: {[l.value for l in status.pending_lenses]}\n"
        f"Synthesis: {'yes' if status.has_synthesis else 'no'}\n"
        f"Total cached analyses: {status.cached_count}"
    )


# =====================================================================
# TOOL 7: export_report
# =====================================================================

@mcp.tool()
async def export_report(
    text_id: str,
    format: str = "markdown",
    ctx: Context = None,
) -> str:
    """Export all analyses and synthesis for a text as a report.

    Args:
        text_id: ID of the text to export.
        format: Output format — "markdown" or "json". Default "markdown".
    """
    state = _state(ctx)
    all_raw = load_all_analyses_for_text(state.db, text_id)
    synth_raw = load_synthesis(state.db, text_id)

    if not all_raw:
        return f"No analyses found for {text_id}. Nothing to export."

    if format == "json":
        report = {
            "text_id": text_id,
            "analyses": {k: json.loads(v) for k, v in all_raw.items()},
            "synthesis": json.loads(synth_raw) if synth_raw else None,
        }
        return f"```json\n{json.dumps(report, indent=2, ensure_ascii=False)}\n```"

    # Markdown format
    lines = [f"# Analysis Report: {text_id}\n"]
    for lens_name, raw_json in sorted(all_raw.items()):
        analysis = LensAnalysis.model_validate_json(raw_json)
        lines.append(f"## {lens_name} Lens\n")
        lines.append(f"{analysis.summary}\n")
        for i, f in enumerate(analysis.findings):
            lines.append(f"### Finding {i + 1}: {f.claim}")
            lines.append(f"**Evidence**: {f.evidence}")
            if f.formal_notation:
                lines.append(f"**Formal**: `{f.formal_notation}`")
            lines.append(f"**Confidence**: {f.confidence}\n")
        if analysis.formal_section:
            lines.append(f"### Formal Section\n{analysis.formal_section}\n")
        lines.append(f"*Vocabulary*: {', '.join(analysis.vocabulary_used)}\n")
        lines.append("---\n")

    if synth_raw:
        from mcp_server.models import Synthesis
        synth = Synthesis.model_validate_json(synth_raw)
        lines.append("## Cross-Lens Synthesis\n")
        lines.append(f"{synth.unified_interpretation}\n")
        if synth.cross_connections:
            lines.append("### Cross-Connections")
            for c in synth.cross_connections:
                lines.append(
                    f"- {c.from_lens.value} ↔ {c.to_lens.value}: "
                    f"{c.relationship} (strength: {c.strength})"
                )
        if synth.emergent_insights:
            lines.append("\n### Emergent Insights")
            for ins in synth.emergent_insights:
                lines.append(f"- {ins}")
        lines.append(f"\n**Overall confidence**: {synth.overall_confidence}")

    return "\n".join(lines)


# =====================================================================
# RESOURCES
# =====================================================================

@mcp.resource("dusun://texts")
def list_texts(ctx: Context = None) -> str:
    """List all available source texts."""
    state = _state(ctx)
    if not state.texts:
        return "No source texts loaded."
    lines = ["# Available Texts\n"]
    for tid, info in sorted(state.texts.items()):
        lines.append(f"- **{tid}**: {info.word_count} words — {info.preview[:100]}...")
    return "\n".join(lines)


@mcp.resource("dusun://texts/{text_id}")
def get_text(text_id: str, ctx: Context = None) -> str:
    """Get the full content of a source text."""
    state = _state(ctx)
    if text_id not in state.text_contents:
        return f"Text '{text_id}' not found."
    return state.text_contents[text_id]


@mcp.resource("dusun://glossary/{lens}")
def get_glossary(lens: str, ctx: Context = None) -> str:
    """Get the domain vocabulary prompt for a specific lens."""
    try:
        lens_enum = LensName(lens)
    except ValueError:
        valid = [l.value for l in LensName]
        return f"Unknown lens '{lens}'. Valid: {valid}"
    return LENS_PROMPTS[lens_enum]


@mcp.resource("dusun://analysis/{text_id}/{lens}")
def get_cached_analysis(text_id: str, lens: str, ctx: Context = None) -> str:
    """Get a cached analysis result."""
    state = _state(ctx)
    raw = load_analysis(state.db, text_id, lens)
    if not raw:
        return f"No cached {lens} analysis for {text_id}."
    return raw


@mcp.resource("dusun://status/{text_id}")
def get_text_status(text_id: str, ctx: Context = None) -> str:
    """Get analysis completion status for a text."""
    state = _state(ctx)
    done = list_analyses_for_text(state.db, text_id)
    has_synth = load_synthesis(state.db, text_id) is not None
    return json.dumps({
        "text_id": text_id,
        "completed_lenses": done,
        "has_synthesis": has_synth,
    }, indent=2)


# =====================================================================
# PROMPT WORKFLOWS
# =====================================================================

@mcp.prompt()
def full_7lens_analysis(text_id: str) -> str:
    """Guide a complete 7-lens analysis workflow for a text.

    This prompt orchestrates: all 7 lens analyses → synthesis.
    """
    lenses = [l.value for l in LensName]
    steps = []
    for i, lens in enumerate(lenses, 1):
        steps.append(
            f"{i}. Call `analyze_with_lens(text_id=\"{text_id}\", lens=\"{lens}\")`"
        )
    steps.append(f"{len(lenses) + 1}. Call `synthesize_analyses(text_id=\"{text_id}\")`")
    steps.append(f"{len(lenses) + 2}. Call `export_report(text_id=\"{text_id}\")` for the full report")

    return (
        f"# Full 7-Lens Analysis Workflow for {text_id}\n\n"
        f"Execute each step in order:\n\n"
        + "\n".join(steps)
        + "\n\nAfter each lens analysis, briefly note the most interesting "
        "finding before proceeding to the next lens. The synthesis step will "
        "then weave all perspectives together."
    )


@mcp.prompt()
def comparative_analysis(text_ids: str, lens: str) -> str:
    """Guide a cross-text comparison workflow.

    Args:
        text_ids: Comma-separated text IDs (e.g. "birinciSoz,ikincisoz").
        lens: Which lens to compare through.
    """
    ids = [t.strip() for t in text_ids.split(",")]
    steps = []
    for i, tid in enumerate(ids, 1):
        steps.append(
            f"{i}. Call `analyze_with_lens(text_id=\"{tid}\", lens=\"{lens}\")`"
        )
    steps.append(
        f"{len(ids) + 1}. Call `compare_texts(text_ids={json.dumps(ids)}, lens=\"{lens}\")`"
    )

    return (
        f"# Comparative Analysis: {lens} lens across {', '.join(ids)}\n\n"
        f"Execute:\n\n" + "\n".join(steps)
    )


@mcp.prompt()
def deep_exploration(text_id: str, lens: str, finding_index: str = "0") -> str:
    """Guide a deep-dive exploration into a specific finding.

    Args:
        text_id: Text to explore.
        lens: Which lens the finding came from.
        finding_index: Index of the finding to deep dive (default "0").
    """
    return (
        f"# Deep Exploration: {text_id} / {lens} / finding #{finding_index}\n\n"
        f"1. First, check status: `get_analysis_status(text_id=\"{text_id}\")`\n"
        f"2. If no {lens} analysis exists, run: `analyze_with_lens(text_id=\"{text_id}\", lens=\"{lens}\")`\n"
        f"3. Deep dive: `deep_dive(text_id=\"{text_id}\", lens=\"{lens}\", finding_index={finding_index})`\n"
        f"4. Review the sub-findings and cross-connections.\n"
        f"5. If a cross-connection looks promising, follow it with another "
        f"`analyze_with_lens` for the connected lens."
    )
