"""llmdebug MCP server — expose crash snapshots and RCA workflows to IDE agents.

Provides tools for programmatic access to debug snapshots and RCA state:
- llmdebug_diagnose: Concise crash summary optimized for LLM consumption
- llmdebug_show: Full expanded JSON snapshot
- llmdebug_list: List available snapshots
- llmdebug_frame: Detailed view of a specific stack frame
- llmdebug_git_context: On-demand enhanced git metadata for crash triage
- llmdebug_diff: Compare two snapshots with structured delta
- llmdebug_hypothesize: Generate ranked root-cause hypotheses
- llmdebug_rca_status / llmdebug_rca_history: RCA state visibility

Run with: llmdebug-mcp (stdio transport for IDE integration)
"""

from __future__ import annotations

import json
import logging
import os
import sys
from typing import Any

# Guard import — mcp is optional
try:
    from mcp.server.fastmcp import FastMCP
except ImportError:
    print(
        "llmdebug MCP server requires: pip install llmdebug[mcp]",
        file=sys.stderr,
    )
    raise SystemExit(1) from None

from ._debug_session import coerce_crash_frame_index
from ._formatting import (
    build_source_text,
    format_enhanced_git_context_text,
    format_local_value,
)
from ._snapshot_loader import (
    find_all_snapshots,
    load_snapshot,
    load_snapshot_raw,
    resolve_snapshot_ref,
)
from .git_context import get_enhanced_git_context
from .rca_state import (
    advance_rca_attempt,
    build_failure_signature,
    derive_session_id,
    get_rca_history,
    get_rca_status,
)
from .snapshot_diff import diff_snapshots, format_diff_text, structured_diff_summary

logger = logging.getLogger("llmdebug.mcp")

mcp = FastMCP("llmdebug")


# ---------------------------------------------------------------------------
# Text formatting helpers (plain-text, no Rich dependency)
# ---------------------------------------------------------------------------


def _format_local_value_str(key: str, value: Any, meta: dict[str, Any] | None) -> str:
    """Format a single local variable as a one-line string for MCP text output."""
    name, type_str, value_str = format_local_value(key, value, meta)
    type_part = f"  ({type_str})" if type_str else ""
    return f"  {name} = {value_str}{type_part}"


def _format_frame_text(frame: dict[str, Any], index: int, is_crash: bool) -> str:
    """Format a single stack frame as plain text."""
    file_rel = frame.get("file_rel", frame.get("file", "?"))
    line = frame.get("line", "?")
    func = frame.get("function", "?")
    prefix = "[CRASH] " if is_crash else ""

    parts: list[str] = [f"{prefix}Frame #{index}: {file_rel}:{line} in {func}"]

    # Source
    source = frame.get("source", {})
    if source:
        parts.append("")
        parts.append("Source:")
        parts.append(build_source_text(source, frame.get("line", 0)))

    # Locals
    locals_dict = frame.get("locals", {})
    locals_meta = frame.get("locals_meta", {})
    if isinstance(locals_dict, dict) and locals_dict:
        parts.append("")
        parts.append("Locals:")
        for k, v in locals_dict.items():
            if k.startswith("@py_"):
                continue
            parts.append(_format_local_value_str(k, v, locals_meta.get(k)))

    return "\n".join(parts)


# format_enhanced_git_context_text imported from ._formatting as format_enhanced_git_context_text


def _effective_gate_mode(gate_mode: str | None) -> str:
    candidate = (gate_mode or os.getenv("LLMDEBUG_RCA_GATE_MODE", "soft")).strip().lower()
    if candidate in {"off", "soft", "strict"}:
        return candidate
    return "soft"


def _extract_evidence_refs(snapshot: dict[str, Any]) -> list[str]:
    refs: list[str] = []
    exc = snapshot.get("exception", {})
    if isinstance(exc, dict):
        exc_type = str(exc.get("type") or "")
        exc_msg = str(exc.get("message") or "")
        if exc_type:
            refs.append(f"exception:{exc_type}")
        if exc_msg:
            refs.append(f"exception_message:{exc_msg[:80]}")

    frames = snapshot.get("frames", [])
    if isinstance(frames, list) and frames:
        crash_idx = coerce_crash_frame_index(snapshot)
        frame: dict[str, Any] | None = None
        if 0 <= crash_idx < len(frames) and isinstance(frames[crash_idx], dict):
            frame = frames[crash_idx]
        elif isinstance(frames[0], dict):
            frame = frames[0]
        if frame:
            file_rel = str(frame.get("file_rel", frame.get("file", "?")))
            line = int(frame.get("line") or 0)
            func = str(frame.get("function") or "?")
            refs.append(f"crash_frame:{file_rel}:{line}:{func}")

    pytest_block = snapshot.get("pytest", {})
    if isinstance(pytest_block, dict):
        nodeid = str(pytest_block.get("nodeid") or "")
        if nodeid:
            refs.append(f"pytest_nodeid:{nodeid}")
    return refs


def _advance_rca(
    *,
    out_dir: str,
    snapshot: dict[str, Any] | None,
    action: str,
    gate_mode: str | None,
    evidence_refs: list[str] | None = None,
    hypothesis: str | None = None,
    experiment_plan: str | None = None,
    proposed_change_summary: str | None = None,
    verification_result: dict[str, Any] | None = None,
    exploratory: bool = False,
    override_reason: str | None = None,
    fallback_stdout: str = "",
    fallback_stderr: str = "",
) -> dict[str, Any] | None:
    try:
        session_id = derive_session_id(snapshot, out_dir=out_dir, fallback="mcp")
        signature = build_failure_signature(
            snapshot,
            stdout=fallback_stdout,
            stderr=fallback_stderr,
        )
        return advance_rca_attempt(
            out_dir=out_dir,
            session_id=session_id,
            action=action,
            failure_signature=signature,
            evidence_refs=evidence_refs or [],
            hypothesis=hypothesis,
            experiment_plan=experiment_plan,
            proposed_change_summary=proposed_change_summary,
            verification_result=verification_result,
            gate_mode=_effective_gate_mode(gate_mode),
            exploratory=exploratory,
            override_reason=override_reason,
        )
    except Exception as exc:  # pragma: no cover - defensive path
        logger.warning("RCA state update failed: %s: %s", type(exc).__name__, exc)
        return None


def _advance_rca_with_snapshot_refs(
    *,
    out_dir: str,
    snapshot: dict[str, Any] | None,
    action: str,
    gate_mode: str | None,
    exploratory: bool = False,
    extra_evidence_refs: list[str] | None = None,
    hypothesis: str | None = None,
    experiment_plan: str | None = None,
    proposed_change_summary: str | None = None,
    verification_result: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
    refs = _extract_evidence_refs(snapshot) if isinstance(snapshot, dict) else []
    if extra_evidence_refs:
        refs.extend(extra_evidence_refs)
    return _advance_rca(
        out_dir=out_dir,
        snapshot=snapshot,
        action=action,
        gate_mode=gate_mode,
        evidence_refs=refs,
        hypothesis=hypothesis,
        experiment_plan=experiment_plan,
        proposed_change_summary=proposed_change_summary,
        verification_result=verification_result,
        exploratory=exploratory,
    )


def _format_tool_response(
    body: str,
    *,
    rca_record: dict[str, Any] | None = None,
    structured_delta: dict[str, Any] | None = None,
) -> str:
    sections = [body]
    if structured_delta is not None:
        structured_text = json.dumps(
            structured_delta,
            indent=2,
            ensure_ascii=False,
        )
        sections.append("## Structured Diff\n" + structured_text)
    if rca_record is not None:
        rca_view = {
            "attempt_id": rca_record.get("attempt_id"),
            "session_id": rca_record.get("session_id"),
            "state": rca_record.get("state"),
            "failure_signature": rca_record.get("failure_signature"),
            "repeat_count": rca_record.get("repeat_count"),
            "allowed_actions": rca_record.get("allowed_actions", []),
            "next_required": rca_record.get("next_required", []),
            "gate_feedback": rca_record.get("gate_feedback", {}),
            "warnings": rca_record.get("gate_warnings", []),
        }
        sections.append("## RCA\n" + json.dumps(rca_view, indent=2, ensure_ascii=False))
        sections.append(
            "## RCA Prompt Contract\n"
            "1) Observed failure (cite evidence)\n"
            "2) Root-cause hypothesis (falsifiable)\n"
            "3) Why evidence supports it\n"
            "4) Minimal fix plan\n"
            "5) Verification plan\n"
            "6) Diff vs previous failed attempt"
        )
    return "\n\n".join(sections)


def _load_snapshot_safe(
    snapshot_path: str | None,
    out_dir: str,
    tool_name: str,
    *,
    raw: bool = False,
) -> dict[str, Any] | str:
    """Load a snapshot, returning an error message string on failure."""
    try:
        return (
            load_snapshot_raw(snapshot_path, out_dir)
            if raw
            else load_snapshot(snapshot_path, out_dir)
        )
    except FileNotFoundError:
        return (
            f"No snapshots found in {out_dir}. "
            "Run your tests with llmdebug installed to capture snapshots."
        )
    except (ValueError, ImportError, json.JSONDecodeError, OSError) as e:
        return f"Error loading snapshot: {type(e).__name__}: {e}"
    except Exception as e:
        logger.error("Unexpected error in %s: %s: %s", tool_name, type(e).__name__, e)
        return f"Error loading snapshot: {type(e).__name__}: {e}"


# ---------------------------------------------------------------------------
# MCP Tools
# ---------------------------------------------------------------------------


@mcp.tool()
def llmdebug_diagnose(
    out_dir: str = ".llmdebug",
    detail: str = "context",
    gate_mode: str = "",
    exploratory: bool = False,
) -> str:
    """Get a concise diagnosis of the latest test failure.

    Returns a structured summary with: exception type/message,
    error category/suggestion, crash frame with source and locals,
    repro command, and git context. Optimized for LLM consumption.

    Args:
        out_dir: Snapshot directory (default: .llmdebug)
        detail: Detail level — "crash" (exception + crash frame only),
                "full" (+ all frames), or "context" (+ repro/git, default)
        gate_mode: RCA gate mode override: off|soft|strict (default from env or soft)
        exploratory: Mark this step as exploratory (used by strict gate override logic)
    """
    result = _load_snapshot_safe(None, out_dir, "llmdebug_diagnose")
    if isinstance(result, str):
        return result
    snapshot = result

    sections: list[str] = []

    # Exception (always included)
    exc = snapshot.get("exception", {})
    exc_type = exc.get("type", "UnknownError")
    exc_msg = exc.get("message", "")
    sections.append(f"## Exception\n{exc_type}: {exc_msg}")

    # Hint (error category — always included)
    ecat = exc.get("error_category", {})
    if ecat:
        cat = ecat.get("category", "")
        sug = ecat.get("suggestion", "")
        hint_parts: list[str] = []
        if cat:
            hint_parts.append(f"Category: {cat}")
        if sug:
            hint_parts.append(f"Suggestion: {sug}")
        if hint_parts:
            sections.append("## Hint\n" + "\n".join(hint_parts))

    # Crash frame (always included)
    frames_obj = snapshot.get("frames", [])
    frames = frames_obj if isinstance(frames_obj, list) else []
    if frames:
        crash_idx = coerce_crash_frame_index(snapshot)
        crash_frame = frames[crash_idx]
        if isinstance(crash_frame, dict):
            sections.append(
                f"## Crash Frame (#{crash_idx})\n"
                + _format_frame_text(crash_frame, crash_idx, True)
            )

        # Additional frames only at "full" or "context" level
        if detail in ("full", "context") and len(frames) > 1:
            for i, frame in enumerate(frames):
                if i == crash_idx or not isinstance(frame, dict):
                    continue
                sections.append(f"## Frame #{i}\n" + _format_frame_text(frame, i, False))
        elif len(frames) > 1:
            sections.append(f"({len(frames) - 1} more frame(s) — use llmdebug_frame to inspect)")

    # Context-level sections: repro, git, env
    if detail in ("full", "context"):
        pt = snapshot.get("pytest", {})
        repro = pt.get("repro")
        if repro and isinstance(repro, list):
            cmd_parts = []
            for r in repro:
                if r.endswith("/python3") or r.endswith("/python"):
                    cmd_parts.append("python")
                else:
                    cmd_parts.append(r)
            sections.append(f"## Repro\n{' '.join(cmd_parts)}")

        git = snapshot.get("git", {})
        if git:
            commit = git.get("commit", "")
            branch = git.get("branch", "")
            dirty = " *dirty*" if git.get("dirty") else ""
            if commit:
                sections.append(f"## Git\n{commit} {branch}{dirty}")

    body = "\n\n".join(sections)
    rca_record = _advance_rca_with_snapshot_refs(
        out_dir=out_dir,
        snapshot=snapshot,
        action="bind_evidence",
        gate_mode=gate_mode,
        exploratory=exploratory,
    )
    return _format_tool_response(body, rca_record=rca_record)


@mcp.tool()
def llmdebug_show(
    out_dir: str = ".llmdebug",
    snapshot_path: str = "",
    detail: str = "context",
    raw_session: bool = False,
    with_rca: bool = False,
    gate_mode: str = "",
    exploratory: bool = False,
) -> str:
    """Show the debug snapshot as expanded JSON.

    Returns snapshot data with all frames, locals, and metadata.
    Use llmdebug_diagnose first for a concise summary,
    then llmdebug_show if you need the full picture.

    Args:
        out_dir: Snapshot directory (default: .llmdebug)
        snapshot_path: Path to a specific snapshot file. If empty, uses latest.
        detail: Detail level — "crash", "full", or "context" (default, everything)
        raw_session: Return raw DebugSession envelope when available.
        with_rca: Return JSON envelope with snapshot + RCA block.
        gate_mode: RCA gate mode override: off|soft|strict.
        exploratory: Mark this step as exploratory.
    """
    result = _load_snapshot_safe(
        snapshot_path or None,
        out_dir,
        "llmdebug_show",
        raw=raw_session,
    )
    if isinstance(result, str):
        return result
    snapshot = result

    # Apply detail filter
    if not raw_session and detail in ("crash", "full", "context"):
        try:
            from .detail_filter import filter_snapshot

            snapshot = filter_snapshot(snapshot, detail)  # type: ignore[arg-type]
        except Exception as e:  # nosec B110 — graceful filter fallback
            logger.warning(
                "filter_snapshot(%r) failed, returning unfiltered: %s: %s",
                detail,
                type(e).__name__,
                e,
            )

    if not with_rca:
        return json.dumps(snapshot, indent=2, ensure_ascii=False, default=str)

    normalized_snapshot = snapshot
    if raw_session:
        try:
            from ._debug_session import normalize_snapshot_view

            normalized_snapshot = normalize_snapshot_view(snapshot)
        except Exception as e:
            logger.warning(
                "normalize_snapshot_view failed, using raw: %s: %s",
                type(e).__name__,
                e,
            )
            normalized_snapshot = snapshot

    rca_record = _advance_rca_with_snapshot_refs(
        out_dir=out_dir,
        snapshot=normalized_snapshot if isinstance(normalized_snapshot, dict) else None,
        action="bind_evidence",
        gate_mode=gate_mode,
        exploratory=exploratory,
    )
    payload = {
        "snapshot": snapshot,
        "rca": rca_record,
    }
    return json.dumps(payload, indent=2, ensure_ascii=False, default=str)


@mcp.tool()
def llmdebug_list(out_dir: str = ".llmdebug", limit: int = 10) -> str:
    """List available debug snapshots, newest first.

    Returns snapshot metadata: timestamp, name, exception type,
    message, and file location. Use snapshot_path from the results
    with llmdebug_show to inspect a specific snapshot.

    Args:
        out_dir: Snapshot directory (default: .llmdebug)
        limit: Maximum number of snapshots to return (default: 10)
    """
    try:
        entries = find_all_snapshots(out_dir)
    except OSError as e:
        return f"Error scanning snapshots: {type(e).__name__}: {e}"
    except Exception as e:
        logger.error("Unexpected error in llmdebug_list: %s: %s", type(e).__name__, e)
        return f"Error scanning snapshots: {type(e).__name__}: {e}"

    if not entries:
        return f"No snapshots found in {out_dir}."

    entries = entries[:limit]

    lines: list[str] = [f"Found {len(entries)} snapshot(s) in {out_dir}:\n"]
    for i, entry in enumerate(entries, 1):
        from datetime import datetime, timezone

        ts = datetime.fromtimestamp(entry["mtime"], tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
        loc = f"{entry['file']}:{entry['line']}"
        msg = entry.get("exc_message", "")
        msg_part = f" — {msg}" if msg else ""
        lines.append(f"  {i}. [{ts}] {entry['name']}  {entry['exc_type']}{msg_part}  ({loc})")
        lines.append(f"     path: {entry['path']}")

    return "\n".join(lines)


@mcp.tool()
def llmdebug_frame(
    index: int = 0,
    out_dir: str = ".llmdebug",
    snapshot_path: str = "",
    gate_mode: str = "",
    exploratory: bool = False,
) -> str:
    """Show a specific stack frame in detail.

    Returns source code, all local variables with types and values,
    and function arguments. Frame 0 is the crash site. Use higher
    indices to inspect callers.

    Args:
        index: Frame index (0 = crash site, higher = callers)
        out_dir: Snapshot directory (default: .llmdebug)
        snapshot_path: Path to a specific snapshot file. If empty, uses latest.
        gate_mode: RCA gate mode override: off|soft|strict.
        exploratory: Mark this step as exploratory.
    """
    result = _load_snapshot_safe(snapshot_path or None, out_dir, "llmdebug_frame")
    if isinstance(result, str):
        return result
    snapshot = result

    frames_obj = snapshot.get("frames", [])
    frames = frames_obj if isinstance(frames_obj, list) else []
    if not frames:
        return "No frames in snapshot."

    if index < 0 or index >= len(frames):
        return (
            f"Frame index {index} out of range. "
            f"Available frames: 0-{len(frames) - 1} ({len(frames)} total)."
        )

    frame = frames[index]
    if not isinstance(frame, dict):
        return f"Frame index {index} is malformed in snapshot."
    crash_idx = coerce_crash_frame_index(snapshot)
    is_crash = index == crash_idx

    body = _format_frame_text(frame, index, is_crash)
    rca_record = _advance_rca_with_snapshot_refs(
        out_dir=out_dir,
        snapshot=snapshot,
        action="bind_evidence",
        gate_mode=gate_mode,
        exploratory=exploratory,
    )
    return _format_tool_response(body, rca_record=rca_record)


@mcp.tool()
def llmdebug_git_context(
    out_dir: str = ".llmdebug",
    snapshot_path: str = "",
    max_commits: int = 5,
    gate_mode: str = "",
    exploratory: bool = False,
) -> str:
    """Show on-demand enhanced git metadata for a snapshot.

    Returns metadata-only context:
    - crash-line blame metadata
    - recent commit metadata
    - crash-file diffstat metadata
    """
    result = _load_snapshot_safe(snapshot_path or None, out_dir, "llmdebug_git_context")
    if isinstance(result, str):
        return result
    snapshot = result

    try:
        max_commits_parsed = int(max_commits)
    except (TypeError, ValueError):
        return f"Invalid max_commits value: {max_commits!r}. Expected an integer between 1 and 50."
    max_commits = max(1, min(max_commits_parsed, 50))
    enhanced = get_enhanced_git_context(snapshot, out_dir=out_dir, max_commits=max_commits)
    if enhanced is None:
        body = "Enhanced git context unavailable (not in a git repository or git is unavailable)."
        rca_record = _advance_rca_with_snapshot_refs(
            out_dir=out_dir,
            snapshot=snapshot,
            action="bind_evidence",
            gate_mode=gate_mode,
            exploratory=exploratory,
        )
        return _format_tool_response(body, rca_record=rca_record)

    evidence_refs: list[str] = []
    head = enhanced.get("head")
    if isinstance(head, dict):
        commit = str(head.get("commit") or "").strip()
        branch = str(head.get("branch") or "").strip()
        if commit:
            evidence_refs.append(f"git_head_commit:{commit}")
        if branch:
            evidence_refs.append(f"git_branch:{branch}")

    crash_file = enhanced.get("crash_file")
    if isinstance(crash_file, dict):
        file_rel = str(crash_file.get("file_rel") or "").strip()
        if file_rel:
            evidence_refs.append(f"git_crash_file:{file_rel}")
        blame = crash_file.get("blame")
        if isinstance(blame, dict):
            blame_commit = str(blame.get("commit") or "").strip()
            if blame_commit:
                evidence_refs.append(f"git_blame_commit:{blame_commit}")

    body = format_enhanced_git_context_text(enhanced)
    rca_record = _advance_rca_with_snapshot_refs(
        out_dir=out_dir,
        snapshot=snapshot,
        action="bind_evidence",
        gate_mode=gate_mode,
        extra_evidence_refs=evidence_refs,
        exploratory=exploratory,
    )
    return _format_tool_response(body, rca_record=rca_record)


@mcp.tool()
def llmdebug_diff(
    old_ref: str = "previous",
    new_ref: str = "latest",
    out_dir: str = ".llmdebug",
    gate_mode: str = "",
    exploratory: bool = False,
) -> str:
    """Compare two snapshots to show what changed between debugging iterations.

    Returns changes in: exception type/message, crash location,
    local variable values, array shapes, and git context.
    Default: compares the two most recent snapshots.

    Args:
        old_ref: Older snapshot — "latest", "previous", "#N" (list index), or file path.
        new_ref: Newer snapshot — same format as old_ref.
        out_dir: Snapshot directory (default: .llmdebug)
        gate_mode: RCA gate mode override: off|soft|strict.
        exploratory: Mark this step as exploratory.
    """
    try:
        old = resolve_snapshot_ref(old_ref, out_dir)
        new = resolve_snapshot_ref(new_ref, out_dir)
    except (FileNotFoundError, ValueError) as e:
        return str(e)
    except (ImportError, json.JSONDecodeError, OSError) as e:
        return f"Error loading snapshots: {type(e).__name__}: {e}"
    except Exception as e:
        logger.error("Unexpected error in llmdebug_diff: %s: %s", type(e).__name__, e)
        return f"Error loading snapshots: {type(e).__name__}: {e}"

    result = diff_snapshots(old, new)
    structured = structured_diff_summary(result)
    verification_status = "fail" if result.failure_signature_same else "inconclusive"
    verification_result = {
        "status": verification_status,
        "checks": ["llmdebug_diff"],
    }
    previous_refs = [f"previous:{ref}" for ref in _extract_evidence_refs(old)]
    rca_record = _advance_rca_with_snapshot_refs(
        out_dir=out_dir,
        snapshot=new,
        action="verify",
        gate_mode=gate_mode,
        extra_evidence_refs=previous_refs,
        verification_result=verification_result,
        proposed_change_summary=(
            "Compared previous and latest failures; inspected exception/location/locals deltas."
        ),
        exploratory=exploratory,
    )
    body = format_diff_text(result)
    return _format_tool_response(body, rca_record=rca_record, structured_delta=structured)


@mcp.tool()
def llmdebug_hypothesize(
    out_dir: str = ".llmdebug",
    snapshot_path: str = "",
    gate_mode: str = "",
    exploratory: bool = False,
) -> str:
    """Generate ranked debugging hypotheses from a snapshot.

    Pattern-matches on exception type, local variable values, array shapes,
    and metadata to produce ranked hypotheses with confidence scores,
    evidence, and suggested next steps.

    Args:
        out_dir: Snapshot directory (default: .llmdebug)
        snapshot_path: Path to a specific snapshot file. If empty, uses latest.
        gate_mode: RCA gate mode override: off|soft|strict.
        exploratory: Mark this step as exploratory.
    """
    result = _load_snapshot_safe(snapshot_path or None, out_dir, "llmdebug_hypothesize")
    if isinstance(result, str):
        return result
    snapshot = result

    from .hypotheses import format_hypotheses_text, generate_hypotheses

    hypotheses = generate_hypotheses(snapshot)
    top_hypothesis = hypotheses[0].description if hypotheses else ""
    experiment_plan = hypotheses[0].suggestion if hypotheses else ""
    hypothesis_refs = list(hypotheses[0].evidence[:3]) if hypotheses else []
    rca_record = _advance_rca_with_snapshot_refs(
        out_dir=out_dir,
        snapshot=snapshot,
        action="hypothesize",
        gate_mode=gate_mode,
        extra_evidence_refs=hypothesis_refs,
        hypothesis=top_hypothesis,
        experiment_plan=experiment_plan,
        exploratory=exploratory,
    )
    body = format_hypotheses_text(hypotheses)
    return _format_tool_response(body, rca_record=rca_record)


@mcp.tool()
def llmdebug_rca_status(
    out_dir: str = ".llmdebug",
    session_id: str = "",
    snapshot_path: str = "",
) -> str:
    """Return latest RCA state for a session (or latest overall if session unspecified)."""
    resolved_session = session_id.strip()
    if not resolved_session and snapshot_path:
        try:
            snap = load_snapshot(snapshot_path, out_dir)
            resolved_session = derive_session_id(snap, out_dir=out_dir, fallback="mcp")
        except Exception as e:
            logger.warning("session ID resolution failed: %s: %s", type(e).__name__, e)
            resolved_session = ""
    elif not resolved_session:
        try:
            snap = load_snapshot(None, out_dir)
            resolved_session = derive_session_id(snap, out_dir=out_dir, fallback="mcp")
        except Exception as e:
            logger.warning("session ID resolution failed: %s: %s", type(e).__name__, e)
            resolved_session = ""

    status = get_rca_status(out_dir=out_dir, session_id=resolved_session or None)
    if status is None:
        if resolved_session:
            return f"No RCA attempts found for session {resolved_session!r} in {out_dir}."
        return f"No RCA attempts found in {out_dir}."
    return json.dumps(status, indent=2, ensure_ascii=False)


@mcp.tool()
def llmdebug_rca_history(
    out_dir: str = ".llmdebug",
    session_id: str = "",
    limit: int = 20,
) -> str:
    """Return RCA history (newest-first) for a session or globally."""
    history = get_rca_history(out_dir=out_dir, session_id=session_id.strip() or None, limit=limit)
    if not history:
        if session_id.strip():
            return f"No RCA attempts found for session {session_id!r} in {out_dir}."
        return f"No RCA attempts found in {out_dir}."
    return json.dumps(history, indent=2, ensure_ascii=False)


@mcp.tool()
def llmdebug_rca_advance(
    action: str,
    out_dir: str = ".llmdebug",
    session_id: str = "",
    snapshot_path: str = "",
    failure_signature: str = "",
    evidence_refs: str = "",
    hypothesis: str = "",
    experiment_plan: str = "",
    proposed_change_summary: str = "",
    verification_status: str = "",
    verification_checks: str = "",
    gate_mode: str = "",
    exploratory: bool = False,
    override_reason: str = "",
) -> str:
    """Manually advance RCA state machine for custom agent workflows."""
    snapshot: dict[str, Any] | None = None
    if snapshot_path:
        try:
            snapshot = load_snapshot(snapshot_path, out_dir)
        except Exception as exc:
            return f"Error loading snapshot: {type(exc).__name__}: {exc}"
    elif not session_id or not failure_signature:
        try:
            snapshot = load_snapshot(None, out_dir)
        except Exception:
            snapshot = None

    resolved_session = session_id.strip()
    if not resolved_session:
        resolved_session = derive_session_id(snapshot, out_dir=out_dir, fallback="manual")
    signature = failure_signature.strip() or build_failure_signature(snapshot)

    refs = [item.strip() for item in evidence_refs.split(",") if item.strip()]
    verification_result: dict[str, Any] | None = None
    if verification_status.strip():
        verification_result = {
            "status": verification_status.strip(),
            "checks": [item.strip() for item in verification_checks.split(",") if item.strip()],
        }

    try:
        record = advance_rca_attempt(
            out_dir=out_dir,
            session_id=resolved_session,
            action=action,
            failure_signature=signature,
            evidence_refs=refs,
            hypothesis=hypothesis.strip() or None,
            experiment_plan=experiment_plan.strip() or None,
            proposed_change_summary=proposed_change_summary.strip() or None,
            verification_result=verification_result,
            gate_mode=_effective_gate_mode(gate_mode),
            exploratory=exploratory,
            override_reason=override_reason.strip() or None,
        )
    except Exception as exc:
        return f"Error updating RCA state: {type(exc).__name__}: {exc}"
    return json.dumps(record, indent=2, ensure_ascii=False)


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


def main() -> None:
    """Run the llmdebug MCP server."""
    logging.basicConfig(
        stream=sys.stderr,
        level=logging.WARNING,
        format="%(name)s: %(message)s",
    )
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
