"""MCP Server — exposes tools for Claude to query the knowledge graph.

Tools:
  - index_project: Index a project directory (can be called from Claude!)
  - query: Hybrid BM25 + vector search over indexed symbols
  - context: 360° view of a symbol (callers, callees, related)
  - impact: Blast radius analysis
  - get_content: Read exact file content
  - list_clusters: Show functional modules in the codebase
  - explore_process: Trace execution flows
"""

from __future__ import annotations

import os
import sys
from pathlib import Path
from typing import TYPE_CHECKING

from mcp.server.fastmcp import FastMCP

from ..core.graph.store import GraphStore
from ..core.search.bm25_index import SymbolSearchIndex

if TYPE_CHECKING:
    from ..core.search.hybrid_search import HybridSearch

mcp = FastMCP(
    "CodeGraph",
    instructions=(
        "Code intelligence server. "
        "If not yet indexed, use 'index_project' first with the project path. "
        "Then use 'query' to find symbols, "
        "'context' for 360° symbol view, 'impact' for blast radius, "
        "'list_clusters' for module overview, 'explore_process' for execution flows, "
        "'get_content' only when you need exact source code."
    ),
)

# Shared state (can start empty — "standby" mode)
_graph: GraphStore | None = None
_search: SymbolSearchIndex | None = None
_hybrid: HybridSearch | None = None
_project_root: str = ""
_indexed: bool = False


def init_mcp(
    graph: GraphStore | None = None,
    search: SymbolSearchIndex | None = None,
    project_root: str = "",
    hybrid: HybridSearch | None = None,
):
    """Initialize MCP server. Can be called with no args for standby mode."""
    global _graph, _search, _hybrid, _project_root, _indexed
    _graph = graph
    _search = search
    _hybrid = hybrid
    _project_root = project_root
    _indexed = bool(search and project_root)


# ─── Project Management Tools ─────────────────────────────────────


@mcp.tool()
def index_project(path: str) -> dict:
    """Index a project directory. Call this FIRST before using other tools.

    Parses all code files, builds knowledge graph, generates embeddings
    for hybrid search. Takes 10-60 seconds depending on project size.

    Args:
        path: Absolute path to the project directory (e.g. "/home/user/my-app" or "C:/Projects/my-app")
    """
    global _graph, _search, _hybrid, _project_root, _indexed

    abs_path = str(Path(path).resolve())
    if not Path(abs_path).is_dir():
        return {"error": f"Directory not found: {abs_path}"}

    try:
        # Phase 1-7: Full ingestion pipeline
        from ..core.ingestion.pipeline import run_pipeline

        index = run_pipeline(abs_path)
        _project_root = abs_path

        # Store in Neo4j (optional)
        graph = GraphStore()
        try:
            graph.connect()
            graph.store_index(index)
            _graph = graph
        except Exception:
            _graph = None

        # Build BM25 search index
        search = SymbolSearchIndex()
        symbols = [
            {
                "uid": n.uid,
                "name": n.name,
                "node_type": n.node_type.value,
                "file_path": n.file_path,
                "line_start": n.line_start,
                "line_end": n.line_end,
                "signature": n.signature,
                "docstring": n.docstring,
                "language": n.language,
            }
            for n in index.nodes
            if n.node_type.value in ("Function", "Class", "Method")
        ]
        search.build(symbols)
        _search = search

        # Try hybrid search
        _hybrid = _try_init_hybrid(search)
        _indexed = True

        stats = index.stats
        result = {
            "status": "ok",
            "project": abs_path,
            "files": stats["files"],
            "symbols": len(symbols),
            "nodes": stats["nodes"],
            "edges": stats["edges"],
            "search_mode": "hybrid" if _hybrid else "keyword",
            "graph": "connected" if _graph else "unavailable",
        }
        return result

    except Exception as e:
        return {"error": f"Indexing failed: {e}"}


@mcp.tool()
def get_status() -> dict:
    """Show current indexing status. Check if a project is loaded and what features are available."""
    return {
        "indexed": _indexed,
        "project": _project_root or None,
        "search": "hybrid" if _hybrid else ("keyword" if _search else "none"),
        "graph": "connected" if _graph else "unavailable",
        "hint": "Use index_project tool to index a project" if not _indexed else "Ready! Use query, context, impact tools.",
    }


# ─── Search & Query Tools ─────────────────────────────────────────


def _check_indexed(tool_name: str) -> dict | None:
    """Return error dict if not indexed, else None."""
    if not _indexed:
        return {
            "error": f"No project indexed yet. Use 'index_project' tool first, then retry '{tool_name}'.",
        }
    return None


@mcp.tool()
def query(
    query: str, limit: int = 5, include_content: bool = False, mode: str = "auto"
) -> dict:
    """Search the code knowledge graph. Returns functions, classes, methods
    matching your query ranked by relevance.

    Uses hybrid search (BM25 + semantic vectors) when available for better
    natural language understanding. Falls back to BM25-only when vectors
    are not configured.

    Args:
        query: What to search for (e.g. "authentication login", "database connection")
        limit: Max results (default 5)
        include_content: If True, include source code body (uses more tokens)
        mode: "auto" (hybrid if available), "keyword" (BM25 only), "semantic" (vector only)
    """
    err = _check_indexed("query")
    if err:
        return err

    search_mode = mode
    if mode == "auto" and _hybrid:
        results = _hybrid.search(query, limit=limit)
        search_mode = "hybrid"
    elif mode == "semantic" and _hybrid:
        results = _hybrid._vector_search(query, limit=limit)
        search_mode = "semantic"
    else:
        results = _search.search(query, limit=limit)
        search_mode = "keyword"

    output = []
    for r in results:
        item = {
            "name": r.get("name"),
            "type": r.get("node_type"),
            "file": r.get("file_path"),
            "line": r.get("line_start"),
            "signature": r.get("signature"),
            "relevance": r.get("_score"),
        }
        if r.get("docstring"):
            item["docstring"] = r["docstring"][:200]
        if include_content and _graph:
            ctx = _graph.get_context(r["uid"])
            item["calls"] = [
                n["name"] for n in ctx["outgoing"].get("CALLS", [])
            ]
            item["called_by"] = [
                n["name"] for n in ctx["incoming"].get("CALLS", [])
            ]
        output.append(item)

    return {"results": output, "total": len(output), "query": query, "mode": search_mode}


@mcp.tool()
def context(
    name: str | None = None,
    uid: str | None = None,
    include_content: bool = False,
) -> dict:
    """Get 360° view of a symbol: who calls it, what it calls,
    inheritance, file location. Use this to understand a symbol deeply.

    Args:
        name: Symbol name (e.g. "login", "UserModel")
        uid: Exact symbol UID (if known, faster)
        include_content: Include source code body
    """
    err = _check_indexed("context")
    if err:
        return err
    if not _graph:
        return {"error": "Graph not available (Neo4j not connected)"}

    # Resolve UID
    if uid:
        sym = _graph.get_symbol(uid)
    elif name:
        matches = _graph.find_symbols(name)
        if not matches:
            return {"error": f"Symbol '{name}' not found", "suggestion": "Use query tool first"}
        if len(matches) > 1:
            return {
                "disambiguation": [
                    {"uid": m["uid"], "name": m["name"], "file": m["file_path"],
                     "type": m["node_type"]}
                    for m in matches
                ],
                "message": f"Found {len(matches)} symbols named '{name}'. Specify uid.",
            }
        sym = matches[0]
        uid = sym["uid"]
    else:
        return {"error": "Provide either 'name' or 'uid'"}

    if not sym:
        return {"error": f"Symbol not found: {uid or name}"}

    ctx = _graph.get_context(uid)

    result = {
        "symbol": {
            "name": sym["name"],
            "type": sym["node_type"],
            "file": sym["file_path"],
            "line": sym.get("line_start"),
            "signature": sym.get("signature"),
        },
        "outgoing": {
            rel: [{"name": n["name"], "file": n["file_path"], "type": n["node_type"]}
                  for n in nodes]
            for rel, nodes in ctx["outgoing"].items()
        },
        "incoming": {
            rel: [{"name": n["name"], "file": n["file_path"], "type": n["node_type"]}
                  for n in nodes]
            for rel, nodes in ctx["incoming"].items()
        },
    }

    if sym.get("docstring"):
        result["symbol"]["docstring"] = sym["docstring"]

    if include_content:
        result["symbol"]["body"] = _read_file_content(
            sym["file_path"], sym.get("line_start"), sym.get("line_end")
        )

    return result


@mcp.tool()
def impact(target: str, direction: str = "downstream", depth: int = 3) -> dict:
    """Blast radius analysis. Shows what symbols are affected if target changes.

    Args:
        target: Symbol name or UID
        direction: 'downstream' (what breaks) or 'upstream' (what depends on this)
        depth: How many hops to traverse (1-5, default 3)
    """
    err = _check_indexed("impact")
    if err:
        return err
    if not _graph:
        return {"error": "Graph not available (Neo4j not connected)"}

    depth = min(max(depth, 1), 5)

    uid = target
    if not target.startswith("file:") and ":" not in target:
        matches = _graph.find_symbols(target)
        if not matches:
            return {"error": f"Symbol '{target}' not found"}
        if len(matches) > 1:
            return {
                "disambiguation": [
                    {"uid": m["uid"], "name": m["name"], "file": m["file_path"]}
                    for m in matches
                ],
            }
        uid = matches[0]["uid"]

    return _graph.get_impact(uid, direction=direction, depth=depth)


@mcp.tool()
def get_content(file_path: str) -> dict:
    """Read exact file content. Use only when you need to write/modify code.
    Prefer 'query' and 'context' for understanding code structure.

    Args:
        file_path: Relative path within the project (e.g. "src/auth/login.py")
    """
    err = _check_indexed("get_content")
    if err:
        return err

    content = _read_file_content(file_path)
    if content is None:
        return {"error": f"File not found: {file_path}"}

    lines = content.count("\n") + 1
    return {"file": file_path, "lines": lines, "content": content}


@mcp.tool()
def list_clusters() -> dict:
    """List all functional modules/clusters in the codebase.
    Shows how code is organized into logical groups with cohesion scores.
    """
    err = _check_indexed("list_clusters")
    if err:
        return err
    if not _graph:
        return {"error": "Graph not available (Neo4j not connected)"}

    clusters = _graph.get_clusters()
    output = []
    for c in clusters:
        members = c.get("members", [])
        output.append({
            "name": c.get("name"),
            "size": len(members),
            "cohesion": c.get("cohesion", 0),
            "members": [
                {"name": m["name"], "type": m["type"], "file": m["file"]}
                for m in members[:15]
            ],
        })
    return {"clusters": output, "total": len(output)}


@mcp.tool()
def explore_process(name: str | None = None, process_id: str | None = None) -> dict:
    """Show a pre-computed execution flow. Use this to understand
    how a feature works end-to-end, from entry point through all call chains.

    Args:
        name: Process name or entry point name (e.g. "login flow", "analyze")
        process_id: Exact process ID (e.g. "process:0")
    """
    err = _check_indexed("explore_process")
    if err:
        return err
    if not _graph:
        return {"error": "Graph not available (Neo4j not connected)"}

    processes = _graph.get_processes()

    if process_id:
        detail = _graph.get_process_detail(process_id)
        if not detail:
            return {"error": f"Process not found: {process_id}"}
        return detail

    if name:
        name_lower = name.lower()
        matches = [
            p for p in processes
            if name_lower in p.get("name", "").lower()
        ]
        if not matches:
            return {
                "error": f"No process matching '{name}'",
                "available": [{"id": p["uid"], "name": p["name"]} for p in processes],
            }
        if len(matches) == 1:
            return _graph.get_process_detail(matches[0]["uid"]) or matches[0]
        return {
            "matches": [
                {"id": p["uid"], "name": p["name"], "steps": p.get("symbol_count", 0)}
                for p in matches
            ],
        }

    return {
        "processes": [
            {
                "id": p.get("uid"),
                "name": p.get("name"),
                "entry_file": p.get("file_path"),
                "depth": p.get("depth", 0),
                "steps": p.get("symbol_count", 0),
            }
            for p in processes
        ],
        "total": len(processes),
    }


# ─── Dashboard ─────────────────────────────────────────────────────


@mcp.tool()
def open_dashboard() -> dict:
    """Start the CodeGraph dashboard web UI and open it in the browser.

    Launches both the API server (port 8000) and Next.js dashboard (port 3000)
    in background, then opens the browser. The MCP server continues running.
    """
    import shutil
    import subprocess

    project = _project_root or "."
    url = "http://localhost:3000"

    try:
        # Find codegraph CLI command
        codegraph_cmd = shutil.which("codegraph")
        if not codegraph_cmd:
            return {
                "error": "codegraph CLI not found. Install with: npm install -g codegraph-smart-mcp",
            }

        # Run `codegraph <project>` in background — starts API + dashboard + opens browser
        subprocess.Popen(
            [codegraph_cmd, project],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
            shell=True,
        )

        return {
            "status": "ok",
            "dashboard": url,
            "api": "http://localhost:8000/api",
            "message": f"Dashboard starting at {url} — browser will open automatically.",
        }
    except Exception as e:
        return {"error": f"Could not start dashboard: {e}"}


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


def _read_file_content(
    file_path: str, line_start: int | None = None, line_end: int | None = None
) -> str | None:
    """Read file content, optionally a specific line range.

    Validates that the resolved path stays within the project root
    to prevent path traversal attacks (e.g. "../../etc/passwd").
    """
    if not _project_root:
        return None

    # Resolve to absolute and check it's within project root
    full_path = Path(os.path.join(_project_root, file_path)).resolve()
    project_root = Path(_project_root).resolve()

    if not str(full_path).startswith(str(project_root)):
        return None  # Path traversal attempt — silently reject

    try:
        with open(full_path, "r", encoding="utf-8", errors="replace") as f:
            if line_start and line_end:
                lines = f.readlines()
                return "".join(lines[max(0, line_start - 1): line_end])
            return f.read()
    except OSError:
        return None


def _try_init_hybrid(bm25_search):
    """Try to initialize hybrid search."""
    from ..core.search.vector_store import VectorStore
    from ..core.search.embedder import Embedder

    try:
        vs = VectorStore()
        count = vs.count()
        if count == 0:
            return None

        embedder = Embedder()
        return HybridSearch(bm25_search, vs, embedder)
    except Exception:
        return None
