"""
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.
  9. hcp_create_workflow  — Create a multi-step workflow chain (E2).
 10. hcp_advance_workflow — Advance a workflow by one stage (E2).
 11. hcp_export_state    — Export full HCP state as JSON checkpoint (E3).
 12. hcp_import_state    — Import a previously exported HCP state checkpoint (E3).

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
from hcp.types import SourceMetadata, WorkflowStage


# ── 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, source_metadata: Optional[str] = None) -> 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.
        source_metadata: Optional JSON object with ingestion hints.
            Accepted keys:
              tool_name (str) — originating tool/plugin name.
              content_type_hint (str) — expected ContentType, e.g. "theological".
              hint_confidence (float) — confidence in the hint [0-1].
              tags (object) — arbitrary string key-value pairs.

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

        meta = None
        if source_metadata is not None:
            try:
                meta_dict = json.loads(source_metadata) if isinstance(source_metadata, str) else source_metadata
                if not isinstance(meta_dict, dict):
                    return json.dumps({
                        "error": "source_metadata must be a JSON object {...}, not a plain string or array.",
                    }, ensure_ascii=False, indent=2)
                meta = SourceMetadata.from_dict(meta_dict)
            except (json.JSONDecodeError, TypeError, KeyError) as exc:
                return json.dumps({
                    "error": f"Invalid source_metadata JSON: {exc}",
                }, ensure_ascii=False, indent=2)

        seed = protocol.ingest(text, source_metadata=meta)

        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)


# =====================================================================
# Tool 9: hcp_create_workflow
# =====================================================================

@mcp.tool()
def hcp_create_workflow(
    stages: str,
    workflow_id: Optional[str] = None,
) -> str:
    """Create a multi-step workflow chain (E2 — typed contracts).

    Each stage defines a typed contract with expected input/output
    and an optional prompt template.  The workflow advances one stage
    at a time via hcp_advance_workflow.

    Args:
        stages: JSON array of stage objects.  Each object must have
                a "name" key; optional keys: "expected_input",
                "expected_output", "prompt_template", "metadata".
        workflow_id: Optional custom workflow ID (auto-generated if omitted).

    Returns:
        JSON with action, workflow_id, n_stages, and current_stage.
    """
    try:
        protocol = _get_hcp()
        stage_dicts = json.loads(stages)
        if not isinstance(stage_dicts, list) or len(stage_dicts) == 0:
            return json.dumps({
                "error": "stages must be a non-empty JSON array."
            }, ensure_ascii=False, indent=2)

        stage_objects: List[WorkflowStage] = []
        for sd in stage_dicts:
            stage_objects.append(WorkflowStage.from_dict(sd))

        state = protocol.create_workflow(stage_objects, workflow_id=workflow_id)

        return json.dumps({
            "action": "create_workflow",
            "workflow_id": state.workflow_id,
            "n_stages": len(state.stages),
            "current_stage": state.current_stage.name if state.current_stage else None,
        }, ensure_ascii=False, indent=2)

    except (json.JSONDecodeError, TypeError, KeyError) as e:
        return json.dumps({
            "error": f"Invalid stages JSON: {e}",
        }, 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 10: hcp_advance_workflow
# =====================================================================

@mcp.tool()
def hcp_advance_workflow(
    workflow_id: str,
    output: str,
    success: bool = True,
    error: Optional[str] = None,
) -> str:
    """Advance a workflow by one stage (E2 — step execution).

    Records the result for the current stage and advances the index.

    Args:
        workflow_id: The workflow ID returned by hcp_create_workflow.
        output: The output produced for the current stage.
        success: Whether the stage succeeded (default: True).
        error: Optional error message if success is False.

    Returns:
        JSON with action, workflow_id, completed_stage, next_stage,
        and is_complete flag.
    """
    try:
        protocol = _get_hcp()
        state = protocol.get_workflow(workflow_id)

        current_name = state.current_stage.name if state.current_stage else None
        protocol.advance_workflow(workflow_id, output, success=success, error=error)

        next_stage = state.current_stage
        return json.dumps({
            "action": "advance_workflow",
            "workflow_id": workflow_id,
            "completed_stage": current_name,
            "next_stage": next_stage.name if next_stage else None,
            "is_complete": state.is_complete,
            "n_results": len(state.results),
        }, ensure_ascii=False, indent=2)

    except KeyError:
        return json.dumps({
            "error": f"Workflow '{workflow_id}' not found.",
        }, ensure_ascii=False, indent=2)
    except RuntimeError as e:
        return json.dumps({
            "error": str(e),
        }, 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 11: hcp_export_state
# =====================================================================

@mcp.tool()
def hcp_export_state() -> str:
    """Export the full HCP state as a JSON checkpoint (E3).

    Captures all ingested chunks, computed seed, seeded chunks,
    workflows, and config.  The exported JSON can be restored later
    via hcp_import_state.

    Per T14 (Ne Ayn Ne Gayr): the snapshot participates in the
    protocol's truth without being identical to it.

    Returns:
        JSON string containing the full HCP state with schema_version.
    """
    try:
        protocol = _get_hcp()
        state = protocol.export_state()
        return json.dumps(state, 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 12: hcp_import_state
# =====================================================================

@mcp.tool()
def hcp_import_state(state_json: str) -> str:
    """Import a previously exported HCP state checkpoint (E3).

    Restores the protocol to the exact state captured by
    hcp_export_state.  Overwrites any current state.

    Args:
        state_json: JSON string from a previous hcp_export_state call.

    Returns:
        JSON confirmation with action, schema_version, n_chunks, and is_ready.
    """
    try:
        protocol = _get_hcp()
        state = json.loads(state_json)
        protocol.import_state(state)
        return json.dumps({
            "action": "import_state",
            "schema_version": state.get("schema_version"),
            "n_chunks": len(protocol._chunks),
            "is_ready": protocol.is_ready,
        }, ensure_ascii=False, indent=2)
    except json.JSONDecodeError as e:
        return json.dumps({
            "error": f"Invalid JSON: {e}",
        }, ensure_ascii=False, indent=2)
    except ValueError as e:
        return json.dumps({
            "error": str(e),
        }, 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"
        }
    }
}

# Framework files deployed by --init.
# Keys = destination relative to cwd, values = filename inside fw_server/data/
_FRAMEWORK_FILES = {
    ".github/prompts/fw.prompt.md": "fw.prompt.md",
    ".github/prompts/fw-tools.prompt.md": "fw-tools.prompt.md",
    ".github/copilot-instructions.md": "copilot-instructions.md",
    "AGENTS.md": "AGENTS.md",
}


def _init_vscode():
    """Bootstrap a workspace: .vscode/mcp.json + framework files for /fw."""
    cwd = Path.cwd()
    data_dir = Path(__file__).resolve().parent / "data"
    created: list[str] = []
    skipped: list[str] = []

    # 1. .vscode/mcp.json
    vscode_dir = cwd / ".vscode"
    config_path = vscode_dir / "mcp.json"
    if config_path.exists():
        skipped.append(str(config_path.relative_to(cwd)))
    else:
        vscode_dir.mkdir(exist_ok=True)
        config_path.write_text(
            json.dumps(_MCP_CONFIG, indent=2) + "\n", encoding="utf-8"
        )
        created.append(str(config_path.relative_to(cwd)))

    # 2. Framework files (prompts, instructions, AGENTS.md)
    for dest_rel, src_name in _FRAMEWORK_FILES.items():
        dest = cwd / dest_rel
        if dest.exists():
            skipped.append(dest_rel)
            continue
        src = data_dir / src_name
        if not src.exists():
            print(f"WARNING: bundled file missing — {src}")
            continue
        dest.parent.mkdir(parents=True, exist_ok=True)
        dest.write_bytes(src.read_bytes())
        created.append(dest_rel)

    # 3. Report
    if created:
        print("Created:")
        for f in created:
            print(f"  {f}")
    if skipped:
        print("Already exists (skipped):")
        for f in skipped:
            print(f"  {f}")
    if created:
        print("\nReload VS Code to activate fw-engine MCP tools and /fw pipeline.")


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()
