"""
fw_server/server.py — MCP server exposing the AGENTS.md framework engine.

Registered tools (callable from VS Code Copilot via @dusun/fw-engine):
  1. run_lens          — Execute one of the 7 lenses and return a LensResult.
  2. run_bileshke      — Run the full composite pipeline (all 7 lenses → QualityReport).
  3. check_kavaid      — Evaluate all 8 kavaid constraints.
  4. verify_chains     — Verify inference chains from the Inference Graph (§IG).
  5. framework_summary — Aggregate framework_summary() from all modules.
  6. hcp_ingest        — Ingest context text into the Holographic Context Protocol.
  7. hcp_query         — Query HCP for relevant chunks.
  8. hcp_diagnostics   — Return HCP diagnostic state.

Design constraints:
  KV₇:  Every tool call is stateless (fresh adapter instances), EXCEPT HCP
         which is classified as AX27 (transparent vessel) and may persist
         across calls within a session.
  AX56: No tool ever returns grade = "Hakkalyakîn".
  AX57: Every response carries a transparency/disclosure block.
  T6/KV₄: Composite scores are always < 1.0.
  AX52: Multiplicative gate — zero in any dimension collapses the whole.

Usage:
  python -m fw_server.server        # starts stdio MCP server
"""

from __future__ import annotations

import json
import os
import sys
import traceback
from pathlib import Path
from typing import Any, Dict, List, Optional

from mcp.server.fastmcp import FastMCP

from fw_server.adapters import ADAPTER_MAP, run_lens
from fw_server.grade_map import grade_to_string, score_to_grade
from fw_server.types import LENS_IDS, VALID_GRADES


# ── HCP instance (AX27 — transparent vessel, persists across calls) ──

_hcp_protocol = None  # lazily initialised


def _get_hcp():
    """Get or create the singleton HCPProtocol instance.

    HCP is classified as AX27 (transparent vessel): it mediates context
    retrieval but doesn't originate meaning.  It persists across calls
    because context ingestion is expensive, and the seed (computed once
    via KV₇-compliant independent process) doesn't leak into lens
    computations.
    """
    global _hcp_protocol
    if _hcp_protocol is None:
        from hcp.protocol import HCPProtocol
        _hcp_protocol = HCPProtocol()
    return _hcp_protocol


# ── Create the MCP server ──

mcp = FastMCP(
    "fw-engine",
    instructions=(
        "AGENTS.md Framework Engine — exposes the 7-lens analytical apparatus, "
        "bileshke composite engine, kavaid constraints, inference chains, and "
        "Holographic Context Protocol as MCP tools."
    ),
)


# =====================================================================
# Tool 1: run_lens
# =====================================================================

@mcp.tool()
def run_single_lens(lens_id: str, params: Optional[str] = None) -> str:
    """Execute one of the 7 formal lenses and return a LensResult.

    Args:
        lens_id: One of "Ontoloji", "Mereoloji", "FOL", "Bayes",
                 "OyunTeorisi", "KategoriTeorisi", "Topoloji + Holografik".
        params: JSON string of lens-specific parameters (optional).
                Examples:
                  FOL: '{"text": "AX1 states that..."}'
                  Ontoloji: '{"kavramlar": [{"name": "tree", "hakikat": [{"isim": "Muhyi", "derece": 0.7}], "mahiyet": "shadow", "dual_class": "harfi"}]}'

    Returns:
        JSON string with score, grade, checks, and detail.
    """
    try:
        parsed_params = json.loads(params) if params else {}
        result = run_lens(lens_id, parsed_params)
        return json.dumps(result.to_dict(), ensure_ascii=False, indent=2)
    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 2: run_bileshke
# =====================================================================

@mcp.tool()
def run_bileshke_pipeline(
    lens_params: Optional[str] = None,
    weights: Optional[str] = None,
    ortam_bits: Optional[str] = None,
) -> str:
    """Run the full composite pipeline: all 7 lenses → bileshke → QualityReport.

    Args:
        lens_params: JSON string — dict mapping lens_id to params dict.
                     Example: '{"FOL": {"text": "AX1..."}, "Ontoloji": {}}'
                     If omitted, all lenses run with empty params.
        weights: JSON string — dict mapping lens_id to float weight.
                 Must sum to 1.0. If omitted, equal weights (1/7 each).
        ortam_bits: JSON string — list of 3 ints (0 or 1) for ortam coverage.
                    Example: '[1, 1, 1]' for full coverage.

    Returns:
        JSON string with composite_score, lens_results, kavaid_checks,
        coverage vectors, warnings, and AX57 disclosure.
    """
    try:
        from bileshke import (
            run_bileshke as _run_bileshke,
            OrtamVektor,
            LensId,
            detect_kv4_warning,
        )
        from bileshke.quality import compute_q3_kavaid, ALL_KAVAID

        # Parse inputs
        all_lens_params: Dict[str, Dict] = (
            json.loads(lens_params) if lens_params else {}
        )
        parsed_weights: Optional[Dict[str, float]] = (
            json.loads(weights) if weights else None
        )
        parsed_ortam: Optional[List[int]] = (
            json.loads(ortam_bits) if ortam_bits else None
        )

        # Run each lens through its adapter (KV₇: each fresh)
        lens_results = []
        errors: Dict[str, str] = {}
        for lid in LENS_IDS:
            p = all_lens_params.get(lid, {})
            try:
                lr = run_lens(lid, p)
                lens_results.append(lr)
            except Exception as e:
                errors[lid] = str(e)

        # Convert string weights to LensId weights if provided
        engine_weights = None
        if parsed_weights:
            engine_weights = {}
            for lid_str, w in parsed_weights.items():
                for lid_enum in LensId:
                    if lid_enum.value == lid_str:
                        engine_weights[lid_enum] = w
                        break

        # Ortam
        ortam = None
        if parsed_ortam and len(parsed_ortam) == 3:
            ortam = OrtamVektor(bits=tuple(parsed_ortam))

        # Run bileshke
        report = _run_bileshke(lens_results, engine_weights, ortam)

        # Compute kavaid (Q-3)
        kavaid_result = compute_q3_kavaid(
            composite_score=report.composite_score,
            latife=report.latife_vektor,
        )

        # Build response
        response = report.to_dict()
        response["kavaid_q3"] = kavaid_result
        if errors:
            response["lens_errors"] = errors

        # AX57 disclosure
        response["ax57_disclosure"] = {
            "composite_score": report.composite_score,
            "grade": grade_to_string(score_to_grade(report.composite_score)),
            "completeness": report.completeness,
            "latife_gate": report.latife_gate,
            "ortam_gate": report.ortam_gate,
            "practical_gate": report.practical_gate,
            "kv4_status": "PASS" if report.composite_score < 0.95 else "FAIL — ≥0.95",
            "kv7_status": "PASS — each lens ran in fresh adapter",
            "ax56_status": "PASS — Hakkalyakîn never claimed",
        }

        return json.dumps(response, ensure_ascii=False, indent=2)

    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 3: check_kavaid
# =====================================================================

@mcp.tool()
def check_kavaid(
    composite_score: float = 0.0,
    latife_bits: Optional[str] = None,
    kavaid_overrides: Optional[str] = None,
) -> str:
    """Evaluate all 8 kavaid constraints and return pass/fail for each.

    Args:
        composite_score: The bileshke composite score (for KV₄ check).
        latife_bits: JSON string — list of 7 ints (0 or 1).
        kavaid_overrides: JSON string — dict of manual overrides
                          (e.g. '{"KV1": true, "KV2": false}').

    Returns:
        JSON with per-kavaid pass/fail, descriptions, all_pass flag,
        and KV₄ warning if applicable.
    """
    try:
        from bileshke.quality import (
            compute_q3_kavaid,
            ALL_KAVAID,
            KAVAID_DESCRIPTIONS,
        )
        from bileshke.types import LatifeVektor
        from bileshke.engine import detect_kv4_warning

        latife = None
        if latife_bits:
            bits = json.loads(latife_bits)
            if len(bits) == 7:
                latife = LatifeVektor(bits=tuple(bits))

        overrides = json.loads(kavaid_overrides) if kavaid_overrides else None

        result = compute_q3_kavaid(
            kavaid_checks=overrides,
            composite_score=composite_score,
            latife=latife,
        )

        # Add descriptions
        result["descriptions"] = KAVAID_DESCRIPTIONS

        # KV₄ explicit check
        kv4_warn = detect_kv4_warning(composite_score)
        if kv4_warn:
            result["kv4_warning"] = kv4_warn

        return json.dumps(result, ensure_ascii=False, indent=2)

    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 4: verify_chains
# =====================================================================

@mcp.tool()
def verify_chains(
    chain_id: Optional[str] = None,
    text: Optional[str] = None,
) -> str:
    """Verify inference chains from the AGENTS.md Inference Graph (§IG).

    Args:
        chain_id: Optional — one of "C1"–"C7". If omitted, all chains verified.
        text: Optional — source text to scan for axiom/theorem/kavaid references.

    Returns:
        JSON with per-chain verification results, hub nodes,
        and (if text provided) extracted axiom references.
    """
    try:
        from fol_formalizasyon import (
            verify_all_chains,
            verify_inference_chain,
            extract_axiom_refs,
            INFERENCE_CHAINS,
            HUB_NODES,
        )

        response: Dict[str, Any] = {}

        # Chain verification
        if chain_id:
            valid, missing = verify_inference_chain(chain_id)
            response["chains"] = {
                chain_id: {"valid": valid, "missing": missing}
            }
        else:
            all_results = verify_all_chains()
            response["chains"] = {
                cid: {"valid": v, "missing": m}
                for cid, (v, m) in all_results.items()
            }

        response["all_valid"] = all(
            v["valid"] for v in response["chains"].values()
        )
        response["hub_nodes"] = dict(HUB_NODES)

        # Axiom ref extraction
        if text:
            refs = extract_axiom_refs(text)
            response["axiom_refs"] = refs
            response["total_refs"] = sum(len(v) for v in refs.values())

        return json.dumps(response, ensure_ascii=False, indent=2)

    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 5: framework_summary
# =====================================================================

@mcp.tool()
def get_framework_summary() -> str:
    """Aggregate framework_summary() from all modules that provide one.

    Returns metadata about each module: lens info, axiom counts,
    capabilities, max scores, etc.  Useful for Stage 5 (Quality Framework)
    and Stage 9B (Synthesis).
    """
    try:
        summaries: Dict[str, Any] = {}

        # bileshke
        try:
            from bileshke.quality import framework_summary as bil_summary
            summaries["bileshke"] = bil_summary()
        except Exception as e:
            summaries["bileshke"] = {"error": str(e)}

        # fol_formalizasyon
        try:
            from fol_formalizasyon import framework_summary as fol_summary
            summaries["fol_formalizasyon"] = fol_summary()
        except Exception as e:
            summaries["fol_formalizasyon"] = {"error": str(e)}

        # bayes_analiz
        try:
            from bayes_analiz import framework_summary as bayes_summary
            summaries["bayes_analiz"] = bayes_summary()
        except Exception as e:
            summaries["bayes_analiz"] = {"error": str(e)}

        # oyun_teorisi
        try:
            from oyun_teorisi import framework_summary as oyun_summary
            summaries["oyun_teorisi"] = oyun_summary()
        except Exception as e:
            summaries["oyun_teorisi"] = {"error": str(e)}

        # kategori_teorisi
        try:
            from kategori_teorisi import framework_summary as kat_summary
            summaries["kategori_teorisi"] = kat_summary()
        except Exception as e:
            summaries["kategori_teorisi"] = {"error": str(e)}

        # holografik
        try:
            from holografik import framework_summary as holo_summary
            summaries["holografik"] = holo_summary()
        except Exception as e:
            summaries["holografik"] = {"error": str(e)}

        return json.dumps(summaries, ensure_ascii=False, indent=2)

    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 6: hcp_ingest
# =====================================================================

@mcp.tool()
def hcp_ingest(text: str) -> str:
    """Ingest context text into the Holographic Context Protocol.

    The HCP seed is computed once and broadcast to every chunk (AX37, KV₆).
    This is classified as AX27 — the protocol is a transparent vessel that
    persists across calls within a session.

    Args:
        text: Raw context text to chunk and process.

    Returns:
        JSON with seed diagnostics and chunk count.
    """
    try:
        protocol = _get_hcp()
        seed = protocol.ingest(text)

        return json.dumps({
            "action": "ingest",
            "n_chunks": seed.n_chunks,
            "coverage_ratio": round(seed.coverage_ratio, 3),
            "n_global_keywords": len(seed.global_keywords),
            "has_structure": seed.has_structure,
            "diagnostics": protocol.diagnostics(),
        }, ensure_ascii=False, indent=2)

    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 7: hcp_query
# =====================================================================

@mcp.tool()
def hcp_query(
    question: str,
    keywords: Optional[str] = None,
    top_k: int = 5,
) -> str:
    """Query the HCP for relevant context chunks.

    Requires prior hcp_ingest call. Uses holographic attention
    (seed-modulated) to rank chunks by relevance.

    Args:
        question: Query text.
        keywords: JSON list of keyword strings (optional; auto-extracted if omitted).
        top_k: Number of top results to return (default: 5).

    Returns:
        JSON list of {content, position, attention_weight, local_importance}.
    """
    try:
        protocol = _get_hcp()
        if not protocol.is_ready:
            return json.dumps({
                "error": "HCP not ready — call hcp_ingest first.",
            }, ensure_ascii=False, indent=2)

        kw_tuple = None
        if keywords:
            kw_list = json.loads(keywords)
            kw_tuple = tuple(kw_list)

        results = protocol.query(question, keywords=kw_tuple, top_k=top_k)

        output = []
        for sc, weight in results:
            output.append({
                "content": sc.chunk.content[:500],  # truncate for MCP
                "position": sc.chunk.position,
                "attention_weight": round(weight, 4),
                "local_importance": round(sc.local_importance, 4),
                "keyword_overlap": list(sc.keyword_overlap),
            })

        return json.dumps({
            "action": "query",
            "question": question,
            "n_results": len(output),
            "results": output,
        }, ensure_ascii=False, indent=2)

    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Tool 8: hcp_diagnostics
# =====================================================================

@mcp.tool()
def hcp_diagnostics() -> str:
    """Return HCP diagnostic state (AX57 — transparency).

    Returns:
        JSON with status, chunk count, type distribution,
        coverage ratio, and structural link info.
    """
    try:
        protocol = _get_hcp()
        return json.dumps(
            protocol.diagnostics(),
            ensure_ascii=False,
            indent=2,
        )
    except Exception as e:
        return json.dumps({
            "error": str(e),
            "traceback": traceback.format_exc(),
        }, ensure_ascii=False, indent=2)


# =====================================================================
# Entry point
# =====================================================================

_MCP_CONFIG = {
    "servers": {
        "fw-engine": {
            "command": "fw-engine",
            "type": "stdio"
        }
    }
}


def _init_vscode():
    """Create .vscode/mcp.json in the current directory."""
    vscode_dir = Path.cwd() / ".vscode"
    config_path = vscode_dir / "mcp.json"

    if config_path.exists():
        print(f"Already exists: {config_path}")
        return

    vscode_dir.mkdir(exist_ok=True)
    config_path.write_text(json.dumps(_MCP_CONFIG, indent=2) + "\n", encoding="utf-8")
    print(f"Created {config_path}")
    print("Reload VS Code to see fw-engine tools in Copilot.")


def main():
    """Start the MCP server on stdio, or run --init to create VS Code config."""
    if "--init" in sys.argv:
        _init_vscode()
        return
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
