#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
PULSE MCP Server — expose the brain via Model Context Protocol.

Runs as a local subprocess (stdio) or HTTP server. Zero hosting cost.
Compatible with: Claude Code, Gemini CLI, Cursor, VS Code Copilot, OpenAI API.

Usage:
    python -m pulse.mcp.server                    # stdio (default)
    python -m pulse.mcp.server --http 8818        # HTTP on port 8818

LAW 7 NOTE: 225 lines — justified keep. Thin MCP endpoint layer: 12 decorated
functions that are each 3-8 lines of glue between the FastMCP decorator and
the real implementation in brain_search, brain_query, knowledge_graph_query,
and pulse_save. No logic lives here — splitting by domain (search/graph/save)
would produce 3 files of ~75 lines each with a shared FastMCP instance that
cannot be split across modules without a registration-pattern rewrite. The
file IS already a thin adapter; further decomposition has no benefit.
"""
from __future__ import annotations

import json
import sys
from pathlib import Path

# Ensure pulse package is importable
ROOT_DIR = Path(__file__).resolve().parent.parent.parent
if str(ROOT_DIR) not in sys.path:
    sys.path.insert(0, str(ROOT_DIR))

from mcp.server.fastmcp import FastMCP

mcp = FastMCP(
    "PULSE Brain",
    instructions="Neural OS memory — search, graph, save. 13 knowledge sources, "
                 "6-dimensional scoring, knowledge graph with causal traversal.",
)


# ---------------------------------------------------------------------------
# Search tools (read-only)
# ---------------------------------------------------------------------------

@mcp.tool()
def pulse_search(query: str, max_per_source: int = 5,
                 task_type: str = "general") -> str:
    """Search the PULSE brain across 13 parallel knowledge sources.

    task_type: general, debug, design, feature, review (affects attention weights).
    Returns ranked results with confidence, salience, and domain scores.
    """
    from pulse.brain.brain_search import brain_search
    results = brain_search(query, max_per_source=max_per_source, task_type=task_type)
    total = results.get("total_results", 0)
    elapsed = results.get("elapsed_ms", 0)
    lines = [f"Found {total} results in {elapsed}ms\n"]
    for source, items in results.get("results", {}).items():
        if not items:
            continue
        lines.append(f"── {source.upper()} ({len(items)} hits) ──")
        for item in items[:max_per_source]:
            text = item.get("text") or item.get("title") or item.get("content", "")
            conf = item.get("confidence", 0)
            sal = item.get("salience", 0)
            lines.append(f"  [{conf:.2f}c {sal:.1f}s] {str(text)[:200]}")
        lines.append("")
    return "\n".join(lines)


@mcp.tool()
def pulse_query(query: str) -> str:
    """Get a formatted knowledge briefing from the PULSE brain.

    Returns prioritized results: anti-patterns, failures, proven patterns,
    domain knowledge, schemas, predictions, and episodic memory.
    """
    from pulse.brain.brain_query import run_query
    return run_query(query)


@mcp.tool()
def pulse_explain(query: str) -> str:
    """Deep explain mode — shows detailed scoring for every search result.

    Includes confidence, salience, Wilson interval, domain competence,
    contradiction flags, and trust scores.
    """
    from pulse.brain.brain_search import brain_search
    from pulse.brain.brain_query import _format_explain
    results = brain_search(query)
    return _format_explain(query, results)


# ---------------------------------------------------------------------------
# Knowledge graph tools (read-only)
# ---------------------------------------------------------------------------

@mcp.tool()
def graph_causes(concept: str, max_results: int = 5) -> str:
    """Find what CAUSES or LEADS_TO a concept in the knowledge graph."""
    from pulse.graph.knowledge_graph_query import query_causes
    results = query_causes(concept, max_results)
    if not results:
        return f"No causal relationships found for: {concept}"
    return "\n".join(
        f"- {r['text']} ({r['relationship']}, conf={r.get('confidence', 0):.2f})"
        for r in results
    )


@mcp.tool()
def graph_prevents(concept: str, max_results: int = 5) -> str:
    """Find what PREVENTS a concept in the knowledge graph."""
    from pulse.graph.knowledge_graph_query import query_prevents
    results = query_prevents(concept, max_results)
    if not results:
        return f"No prevention relationships found for: {concept}"
    return "\n".join(
        f"- {r['text']} ({r['relationship']}, conf={r.get('confidence', 0):.2f})"
        for r in results
    )


@mcp.tool()
def graph_related(concept: str, max_results: int = 5) -> str:
    """Find semantically related concepts via spreading activation."""
    from pulse.graph.knowledge_graph_query import query_related
    results = query_related(concept, max_results)
    if not results:
        return f"No related concepts found for: {concept}"
    return "\n".join(
        f"- {r['text']} (type={r.get('type', '?')}, hops={r.get('hop_distance', '?')})"
        for r in results
    )


@mcp.tool()
def graph_chain(from_concept: str, to_concept: str, max_depth: int = 4) -> str:
    """Find the causal chain between two concepts (BFS path-finding)."""
    from pulse.graph.knowledge_graph_query import query_chain
    path = query_chain(from_concept, to_concept, max_depth)
    if not path:
        return f"No path found between '{from_concept}' and '{to_concept}'"
    return " → ".join(f"{n['text']} ({n.get('type', '?')})" for n in path)


@mcp.tool()
def graph_stats() -> str:
    """Get knowledge graph statistics: nodes, edges, types."""
    from pulse.graph.knowledge_graph_query import get_graph_stats
    stats = get_graph_stats()
    lines = [
        f"Nodes: {stats.get('total_nodes', 0)}",
        f"Edges: {stats.get('total_edges', 0)}",
        f"Built: {stats.get('built_at', 'unknown')}",
    ]
    for label, data in [("Node types", stats.get("node_types", {})),
                        ("Edge types", stats.get("edge_types", {}))]:
        if data:
            lines.append(f"\n{label}:")
            for k, v in sorted(data.items(), key=lambda x: -x[1]):
                lines.append(f"  {k}: {v}")
    return "\n".join(lines)


@mcp.tool()
def graph_hubs(top_n: int = 10) -> str:
    """Get the most connected nodes in the knowledge graph."""
    from pulse.graph.knowledge_graph_query import get_hub_nodes
    hubs = get_hub_nodes(top_n)
    if not hubs:
        return "No hub nodes found"
    return "\n".join(
        f"- {h['text']} ({h.get('type', '?')}, {h.get('connections', 0)} connections)"
        for h in hubs
    )


# ---------------------------------------------------------------------------
# Save tool (write — requires agent identity)
# ---------------------------------------------------------------------------

@mcp.tool()
def pulse_save(agent_name: str, learnings: str = "", failures: str = "",
               domain: str = "general", task_id: str = "") -> str:
    """Save learnings and failures to the PULSE brain.

    agent_name: Your identifier (e.g., 'claude', 'gemini', 'cursor').
    learnings: Comma-separated list of things that worked.
    failures: Comma-separated list of things that failed.
    domain: Task domain (e.g., 'architecture', 'testing', 'database').
    task_id: Optional task board ID to mark done.
    """
    from pulse.brain.pulse_save import save
    learn_list = [s.strip() for s in learnings.split(",") if s.strip()] if learnings else []
    fail_list = [s.strip() for s in failures.split(",") if s.strip()] if failures else []
    results = save(
        agent_name=agent_name, task_id=task_id,
        learnings=learn_list, failures=fail_list, domain=domain,
    )
    return json.dumps(results, indent=2)


# ---------------------------------------------------------------------------
# Resources (read-only data)
# ---------------------------------------------------------------------------

@mcp.resource("pulse://brain/config")
def brain_config() -> str:
    """Current brain configuration (search params, thresholds, etc)."""
    from pulse.core.brain_utils import load_config
    return json.dumps(load_config(), indent=2)


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

def main():
    import argparse
    parser = argparse.ArgumentParser(description="PULSE MCP Server")
    parser.add_argument("--http", type=int, default=0,
                        help="Run as HTTP server on this port (0 = stdio)")
    args = parser.parse_args()
    if args.http:
        mcp.run(transport="streamable-http", host="127.0.0.1", port=args.http)
    else:
        mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
