"""MCP Server for cokodo-agent.

Exposes .agent/ protocol context via Model Context Protocol,
enabling IDE AI clients to programmatically access project data
and update project state.

Requires: pip install cokodo-agent[mcp]
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

from mcp.server.fastmcp import FastMCP

from cokodo_agent.context import (
    build_context_content,
    get_context_files,
    list_project_files,
    read_status,
    update_status_section,
)

_RESOURCE_FILES: dict[str, str] = {
    "status": "project/status.md",
    "context": "project/context.md",
    "deploy": "project/deploy.md",
    "tech-stack": "project/tech-stack.md",
    "commands": "project/commands.md",
    "known-issues": "project/known-issues.md",
}


_NO_PROJECT_MSG = (
    "No .agent/ directory found in the current project. "
    "Run 'co init' to initialize the protocol."
)


def create_server(agent_dir: Path | None = None) -> FastMCP:
    """Create and configure the MCP server.

    The server always starts successfully. If no .agent/ directory is found,
    tools return a helpful error message instead of crashing the server process.

    Args:
        agent_dir: Path to .agent/ directory. If None, searches from cwd.
    """
    if agent_dir is None:
        try:
            agent_dir = _find_agent_dir()
        except FileNotFoundError:
            agent_dir = None
    elif not agent_dir.is_dir():
        agent_dir = None

    # Capture as a local variable for closures; may be None if no project found.
    _dir = agent_dir

    mcp = FastMCP(
        "cokodo-agent",
        instructions=(
            "This server provides access to the AI Agent Collaboration Protocol (.agent/ directory). "
            "Use get_project_context as your FIRST action to load all relevant project files. "
            "Use update_status after completing tasks or before git commits."
        ),
    )

    for _name, _rel_path in _RESOURCE_FILES.items():

        def _make_reader(name: str, path: str, d: Path | None) -> Any:
            def reader() -> str:
                if d is None:
                    return _NO_PROJECT_MSG
                full = d / path
                if not full.exists():
                    return f"File not found: {path}"
                return full.read_text(encoding="utf-8")

            reader.__name__ = f"read_{name.replace('-', '_')}"
            reader.__doc__ = f"Current content of .agent/{path}"
            return reader

        mcp.resource(f"agent://{_name}")(
            _make_reader(_name, _rel_path, _dir)
        )

    @mcp.resource("agent://file/{path}")
    def read_agent_file(path: str) -> str:
        """Read any file from the .agent/ directory by relative path."""
        if _dir is None:
            return _NO_PROJECT_MSG
        safe_path = Path(path)
        if ".." in safe_path.parts:
            return "Error: path traversal not allowed"
        full = _dir / safe_path
        if not full.exists():
            return f"File not found: {path}"
        try:
            return full.read_text(encoding="utf-8")
        except OSError as e:
            return f"Error reading {path}: {e}"

    @mcp.tool()
    def get_project_context(
        stack: str | None = None,
        task: str | None = None,
    ) -> str:
        """Get all relevant project context files based on stack and task type.

        Call this as your FIRST action in every conversation to load project context.

        Args:
            stack: Tech stack filter (python/rust/qt/mixed). Optional.
            task: Task type or profile (coding/testing/review/documentation/
                  bug_fix/feature_development/code_review). Optional.

        Returns:
            Concatenated contents of all relevant .agent/ files.
        """
        if _dir is None:
            return _NO_PROJECT_MSG

        files = get_context_files(_dir, stack=stack, task=task)
        if not files:
            return "No files found in manifest.json loading_strategy."

        content_map = build_context_content(_dir, files)
        if not content_map:
            return "No context files could be read."

        parts: list[str] = []
        for rel_path, content in content_map.items():
            parts.append(f"# {rel_path}\n\n{content}")

        parts.append(
            "---\n\n"
            "# Session Rules\n\n"
            "Follow these rules for this session:\n\n"
            "1. Update `status.md` at checkpoints — call `update_status` after completing tasks, "
            "before git commits, when switching tasks, or when blockers change.\n"
            "2. No external CDN links — all resources must be local to the project.\n"
            "3. UTF-8 encoding — always explicitly declare UTF-8 when reading/writing files.\n"
            "4. Forward slashes — use `/` as path separator in all commands.\n"
            "5. Test data isolation — automated tests must use `autotest_` prefix.\n"
            "6. Engine/Instance separation — never put project-specific info in `core/` files.\n"
        )

        return "\n---\n\n".join(parts)

    @mcp.tool()
    def update_status(
        completed_tasks: list[str] | None = None,
        new_tasks: list[str] | None = None,
        blockers: list[str] | None = None,
        session_context: str | None = None,
    ) -> str:
        """Update project/status.md at a checkpoint.

        Call this after completing tasks, before git commits,
        when switching tasks, or when blockers change.

        Args:
            completed_tasks: Task descriptions to mark as done.
            new_tasks: New task descriptions to add to Pending.
            blockers: Current blockers (replaces existing). Pass empty list to clear.
            session_context: Key info for next session (replaces Session Context section).

        Returns:
            Updated status.md content, or error message.
        """
        if _dir is None:
            return _NO_PROJECT_MSG
        return update_status_section(
            _dir,
            completed_tasks=completed_tasks,
            new_tasks=new_tasks,
            blockers=blockers,
            session_context=session_context,
        )

    @mcp.tool()
    def lint_protocol() -> str:
        """Run protocol compliance check on the .agent/ directory.

        Returns a summary of lint results (passed/failed rules).
        """
        if _dir is None:
            return _NO_PROJECT_MSG

        from collections import defaultdict

        from cokodo_agent.linter import ProtocolLinter

        linter = ProtocolLinter(_dir)
        all_results = linter.lint_all()

        by_rule: dict[str, list] = defaultdict(list)
        for r in all_results:
            by_rule[r.rule].append(r)

        lines: list[str] = []
        total_pass = 0
        total_fail = 0
        for rule_name, rule_results in sorted(by_rule.items()):
            passed = sum(1 for r in rule_results if r.passed)
            failed = len(rule_results) - passed
            total_pass += passed
            total_fail += failed
            status = "OK" if failed == 0 else "FAIL"
            lines.append(f"[{status}] {rule_name}: {passed}/{len(rule_results)} passed")
            if failed > 0:
                for r in rule_results:
                    if not r.passed:
                        lines.append(f"  x {r.file or ''}: {r.message}")

        lines.append(f"\nTotal: {total_pass}/{total_pass + total_fail} passed")
        return "\n".join(lines)

    @mcp.tool()
    def list_files() -> str:
        """List all files in .agent/ directory with their loading layer and description.

        Useful for discovering what project information is available.
        """
        if _dir is None:
            return _NO_PROJECT_MSG

        files = list_project_files(_dir)
        if not files:
            return "No files found in .agent/ directory."

        lines: list[str] = []
        current_layer = ""
        for f in sorted(files, key=lambda x: (x["layer"], x["path"])):
            if f["layer"] != current_layer:
                current_layer = f["layer"]
                lines.append(f"\n## {current_layer}")
            lines.append(f"- {f['path']}: {f['description']}")
        return "\n".join(lines)

    @mcp.tool()
    def list_sops() -> str:
        """List all available Standard Operating Procedures (SOPs) for this project.

        SOPs are step-by-step guides for recurring tasks specific to this project
        (e.g., how to release, how to debug, git workflow conventions).

        Call this when you need to know how to perform a specific operation,
        or before starting any task that might have a documented procedure.

        Returns:
            List of available SOPs with their names and trigger descriptions.
        """
        if _dir is None:
            return _NO_PROJECT_MSG

        sop_dir = _dir / "project" / "sop"
        if not sop_dir.exists():
            return "No SOP directory found at project/sop/. No SOPs have been defined yet."

        sop_files = sorted(sop_dir.glob("*.md"))
        if not sop_files:
            return "No SOP files found in project/sop/."

        lines = ["# Available SOPs\n"]
        lines.append("Use `get_sop(name)` to read a specific SOP.\n")
        for f in sop_files:
            # Extract trigger line from file (line starting with "> **Trigger**:")
            trigger = ""
            try:
                for line in f.read_text(encoding="utf-8").splitlines():
                    if "**Trigger**:" in line:
                        trigger = line.strip().lstrip(">").strip()
                        break
            except OSError:
                pass
            lines.append(f"- **{f.stem}** (`project/sop/{f.name}`)")
            if trigger:
                lines.append(f"  {trigger}")
        return "\n".join(lines)

    @mcp.tool()
    def get_sop(name: str) -> str:
        """Read a specific Standard Operating Procedure (SOP).

        Call list_sops() first to discover available SOPs.

        Use this before performing any operation that likely has a documented
        procedure (release, debug, git operations, etc.).

        Args:
            name: SOP name (e.g., "release", "debug", "git-workflow").
                  Can omit the .md extension.

        Returns:
            Full SOP content, or an error message if not found.
        """
        if _dir is None:
            return _NO_PROJECT_MSG

        sop_dir = _dir / "project" / "sop"
        if not sop_dir.exists():
            return "No SOP directory found at project/sop/."

        # Normalize name: strip .md if provided
        stem = name.removesuffix(".md")

        # Exact match first
        sop_path = sop_dir / f"{stem}.md"
        if not sop_path.exists():
            # Try fuzzy: find any file whose stem contains the query
            candidates = [f for f in sop_dir.glob("*.md") if stem.lower() in f.stem.lower()]
            if len(candidates) == 1:
                sop_path = candidates[0]
            elif len(candidates) > 1:
                names = ", ".join(f.stem for f in candidates)
                return f"Ambiguous SOP name '{name}'. Matches: {names}. Be more specific."
            else:
                available = ", ".join(f.stem for f in sorted(sop_dir.glob("*.md")))
                return f"SOP '{name}' not found. Available: {available}"

        try:
            return sop_path.read_text(encoding="utf-8")
        except OSError as e:
            return f"Error reading SOP '{name}': {e}"

    # ------------------------------------------------------------------
    # Cross-project reference tools
    # ------------------------------------------------------------------

    @mcp.tool()
    def list_relations() -> str:
        """List all declared project relationships (references and collaborations).

        Returns names, types, source paths, and accessibility status for each
        declared relationship. Use this to discover what cross-project context
        is available.
        """
        if _dir is None:
            return _NO_PROJECT_MSG

        from cokodo_agent.relations import (
            check_collaborations,
            check_references,
            load_collaborations,
            load_references,
        )

        project_root = _dir.parent
        refs = load_references(_dir)
        collabs = load_collaborations(_dir)

        if not refs and not collabs:
            return "No cross-project relationships declared in manifest.json."

        lines: list[str] = []

        if refs:
            statuses = check_references(_dir, project_root)
            status_map = {s.name: s for s in statuses}

            lines.append("# References\n")
            for r in refs:
                st = status_map.get(r.name)
                health = "OK" if (st and st.accessible) else "INACCESSIBLE"
                stype = st.source_type if st else "unknown"
                lines.append(f"- **{r.name}** ({stype}, {health})")
                if r.description:
                    lines.append(f"  {r.description}")
                lines.append(f"  source: {r.source}")
                if r.scope:
                    lines.append(f"  scope: {', '.join(r.scope)}")
                lines.append(f"  use_when: {r.use_when}")
                lines.append("")

        if collabs:
            cstatuses = check_collaborations(_dir, project_root)
            cstatus_map = {s.name: s for s in cstatuses}

            lines.append("# Collaborations\n")
            for c in collabs:
                cst = cstatus_map.get(c.name)
                if cst and cst.partner_accessible:
                    health = "IN_SYNC" if cst.shared_files_in_sync else "DRIFTED"
                elif cst:
                    health = "INACCESSIBLE"
                else:
                    health = "UNKNOWN"
                lines.append(f"- **{c.name}** (role: {c.role}, {health})")
                if c.description:
                    lines.append(f"  {c.description}")
                lines.append(f"  partner: {c.partner}")
                if c.shared_scope:
                    lines.append(f"  shared_scope: {', '.join(c.shared_scope)}")
                lines.append("")

        return "\n".join(lines)

    @mcp.tool()
    def get_related_context(
        relation: str,
        file: str | None = None,
    ) -> str:
        """Get context from a related project, document, or directory.

        Use list_relations first to discover available references.

        Args:
            relation: Name of the declared reference (from manifest.json).
            file: Specific file to read (relative path). If omitted, returns
                  all files in scope concatenated.

        Returns:
            Content of the referenced file(s), or an error message.
        """
        if _dir is None:
            return _NO_PROJECT_MSG

        from cokodo_agent.relations import (
            get_reference_by_name,
            resolve_reference_content,
        )

        project_root = _dir.parent
        ref = get_reference_by_name(_dir, relation)
        if ref is None:
            return f"Reference '{relation}' not found. Use list_relations to see available references."

        try:
            content_map = resolve_reference_content(ref, project_root, file=file)
        except FileNotFoundError as e:
            return f"Error: {e}"

        if not content_map:
            return f"No matching files found in reference '{relation}'."

        parts: list[str] = []
        for rel_path, content in content_map.items():
            parts.append(f"# [ref:{relation}] {rel_path}\n\n{content}")
        return "\n---\n\n".join(parts)

    @mcp.tool()
    def get_collaboration_context(
        collaboration: str,
        file: str | None = None,
    ) -> str:
        """Get shared content from a collaboration partner.

        Use list_relations first to discover available collaborations.

        Args:
            collaboration: Name of the declared collaboration.
            file: Specific file to read (relative path). If omitted, returns
                  all shared-scope files concatenated.

        Returns:
            Content of the shared file(s), or an error message.
        """
        if _dir is None:
            return _NO_PROJECT_MSG

        from cokodo_agent.relations import (
            get_collaboration_by_name,
            resolve_collaboration_content,
        )

        project_root = _dir.parent
        c = get_collaboration_by_name(_dir, collaboration)
        if c is None:
            return (
                f"Collaboration '{collaboration}' not found. "
                "Use list_relations to see available collaborations."
            )

        try:
            content_map = resolve_collaboration_content(c, project_root, file=file)
        except FileNotFoundError as e:
            return f"Error: {e}"

        if not content_map:
            return f"No matching files found in collaboration '{collaboration}'."

        parts: list[str] = []
        for rel_path, content in content_map.items():
            parts.append(f"# [collab:{collaboration}] {rel_path}\n\n{content}")
        return "\n---\n\n".join(parts)

    @mcp.tool()
    def get_collaboration_status(
        collaboration: str | None = None,
    ) -> str:
        """Get sync status and file diff for collaborations.

        Args:
            collaboration: Specific collaboration name. If omitted, checks all.

        Returns:
            Detailed sync status including which files have drifted.
        """
        if _dir is None:
            return _NO_PROJECT_MSG

        from cokodo_agent.relations import (
            check_collaborations,
            diff_collaboration,
            get_collaboration_by_name,
            load_collaborations,
        )

        project_root = _dir.parent

        if collaboration:
            c = get_collaboration_by_name(_dir, collaboration)
            if c is None:
                return f"Collaboration '{collaboration}' not found."
            collabs = [c]
        else:
            collabs = load_collaborations(_dir)

        if not collabs:
            return "No collaborations declared."

        statuses = check_collaborations(_dir, project_root)
        status_map = {s.name: s for s in statuses}

        lines: list[str] = []
        for c in collabs:
            st = status_map.get(c.name)
            lines.append(f"## {c.name} (role: {c.role})")

            if not st or not st.partner_accessible:
                lines.append(f"  Status: INACCESSIBLE")
                if st and st.error:
                    lines.append(f"  Error: {st.error}")
                lines.append("")
                continue

            if st.shared_files_in_sync:
                lines.append("  Status: IN_SYNC")
            else:
                lines.append("  Status: DRIFTED")
                try:
                    diffs = diff_collaboration(c, project_root, _dir)
                    for fpath, fstatus in diffs.items():
                        if fstatus != "in_sync":
                            lines.append(f"  - {fpath}: {fstatus}")
                except FileNotFoundError:
                    pass
            lines.append("")

        return "\n".join(lines)

    @mcp.tool()
    def fetch_remote_references() -> str:
        """Fetch or refresh all remote (git:...) references.

        Downloads remote repository content to local cache so that
        get_related_context can serve it. Remote references that are
        already cached and fresh are skipped unless they are stale.

        Returns a summary of fetch results.
        """
        if _dir is None:
            return _NO_PROJECT_MSG

        from cokodo_agent.relations import load_references
        from cokodo_agent.remote import (
            fetch_remote_source,
            is_remote_source,
            parse_remote_source,
        )

        refs = load_references(_dir)
        remote_refs = [r for r in refs if is_remote_source(r.source)]

        if not remote_refs:
            return "No remote references declared."

        lines: list[str] = []
        for r in remote_refs:
            try:
                parsed = parse_remote_source(r.source)
                fetch_remote_source(parsed, force=False)
                lines.append(f"[OK] {r.name}: cached ({parsed.owner}/{parsed.repo})")
            except (ConnectionError, FileNotFoundError, ValueError) as e:
                lines.append(f"[FAIL] {r.name}: {e}")

        return "\n".join(lines)

    @mcp.tool()
    def check_relation_health() -> str:
        """Check if all declared relationships are accessible.

        Returns a summary showing which references and collaborations are
        reachable and which have errors. Useful for diagnosing broken
        cross-project links.
        """
        if _dir is None:
            return _NO_PROJECT_MSG

        from cokodo_agent.relations import (
            check_collaborations,
            check_references,
            load_collaborations,
            load_references,
        )

        project_root = _dir.parent
        refs = load_references(_dir)
        collabs = load_collaborations(_dir)

        if not refs and not collabs:
            return "No relationships declared. Nothing to check."

        lines: list[str] = []
        ok_count = 0
        fail_count = 0

        if refs:
            lines.append("## References")
            statuses = check_references(_dir, project_root)
            for st in statuses:
                if st.accessible:
                    lines.append(f"[OK]   {st.name} -> {st.source_type} ({st.resolved_path})")
                    ok_count += 1
                else:
                    lines.append(f"[FAIL] {st.name}: {st.error}")
                    fail_count += 1
            lines.append("")

        if collabs:
            lines.append("## Collaborations")
            cstatuses = check_collaborations(_dir, project_root)
            for cst in cstatuses:
                if cst.partner_accessible:
                    sync = "in_sync" if cst.shared_files_in_sync else "drifted"
                    lines.append(f"[OK]   {cst.name} ({cst.role}) -> {sync}")
                    ok_count += 1
                else:
                    lines.append(f"[FAIL] {cst.name} ({cst.role}): {cst.error}")
                    fail_count += 1
            lines.append("")

        lines.append(f"\n{ok_count}/{ok_count + fail_count} relationships healthy.")
        return "\n".join(lines)

    # ------------------------------------------------------------------
    # Global registry tools (available in all server modes)
    # ------------------------------------------------------------------

    _register_global_tools(mcp)

    # ------------------------------------------------------------------
    # Prompts
    # ------------------------------------------------------------------

    @mcp.prompt()
    def session_init(
        stack: str | None = None,
        task: str | None = None,
    ) -> str:
        """Build a complete session initialization prompt with all relevant project context.

        Call this at the start of any work session to load full project state.

        Args:
            stack: Tech stack filter (python/rust/qt/mixed). Optional.
            task: Task type (coding/testing/review/documentation/bug_fix/
                  feature_development/code_review). Optional.
        """
        if _dir is None:
            return _NO_PROJECT_MSG

        from cokodo_agent.config import BUNDLED_PROTOCOL_VERSION, VERSION

        files = get_context_files(_dir, stack=stack, task=task)
        content_map = build_context_content(_dir, files)

        # Extract project name from context.md if available
        project_name = _dir.parent.name
        ctx_path = _dir / "project" / "context.md"
        if ctx_path.exists():
            for line in ctx_path.read_text(encoding="utf-8").splitlines():
                if line.startswith("## Project Name"):
                    continue
                if line.strip() and not line.startswith("#") and not line.startswith(">") and "{{" not in line:
                    project_name = line.strip()
                    break

        # Extract active tasks from status.md
        active_tasks: list[str] = []
        status_path = _dir / "project" / "status.md"
        if status_path.exists():
            in_progress = False
            for line in status_path.read_text(encoding="utf-8").splitlines():
                if "### In Progress" in line:
                    in_progress = True
                    continue
                if in_progress:
                    if line.startswith("###"):
                        break
                    if line.startswith("- ") and "(none)" not in line:
                        active_tasks.append(line.lstrip("- ").strip())

        parts: list[str] = [
            f"# Session Context — {project_name}\n",
            f"> Protocol: v{BUNDLED_PROTOCOL_VERSION} | CLI: v{VERSION}"
            + (f" | Stack: {stack}" if stack else "")
            + (f" | Task: {task}" if task else "")
            + "\n",
            "The following project context has been loaded from the .agent/ protocol.\n",
        ]

        for rel_path, content in content_map.items():
            parts.append(f"## {rel_path}\n\n{content}\n")

        from cokodo_agent.relations import (
            load_collaborations,
            load_references,
            resolve_collaboration_content,
            resolve_reference_content,
        )

        project_root = _dir.parent
        refs = load_references(_dir)
        always_refs = [r for r in refs if r.use_when == "always"]
        collabs = load_collaborations(_dir)

        if always_refs or collabs:
            parts.append("\n---\n# Cross-Project Context (auto-loaded)\n")

            for ref in always_refs:
                try:
                    ref_content = resolve_reference_content(ref, project_root)
                    for rel_path, content in ref_content.items():
                        parts.append(f"## [ref:{ref.name}] {rel_path}\n\n{content}\n")
                except FileNotFoundError:
                    parts.append(f"## [ref:{ref.name}] (inaccessible)\n")

            for c in collabs:
                try:
                    collab_content = resolve_collaboration_content(c, project_root)
                    for rel_path, content in collab_content.items():
                        parts.append(
                            f"## [collab:{c.name}] {rel_path}\n\n{content}\n"
                        )
                except FileNotFoundError:
                    parts.append(f"## [collab:{c.name}] (inaccessible)\n")

        if active_tasks:
            parts.append("\n---\n# Current Focus\n")
            parts.append("The following tasks are currently in progress:\n")
            for t in active_tasks:
                parts.append(f"- {t}")
            parts.append("")

        parts.append(
            "\n---\n"
            "# Behavior Rules\n\n"
            "Follow these rules for this session:\n\n"
            "1. **Read status.md first** — understand current phase and pending tasks before acting.\n"
            "2. **Update status at checkpoints** — call `update_status` after completing tasks, "
            "before git commits, when switching tasks, or when blockers are found/resolved.\n"
            "3. **No external CDN links** — all resources must be local to the project.\n"
            "4. **UTF-8 encoding** — always explicitly declare UTF-8 when reading/writing files.\n"
            "5. **Forward slashes** — use `/` as path separator in all commands.\n"
            "6. **Test data isolation** — automated tests must use `autotest_` prefix for generated data.\n"
            "7. **Engine/Instance separation** — never put project-specific info in `core/` files.\n"
        )

        return "\n".join(parts)

    @mcp.prompt()
    def start_task(
        task_name: str | None = None,
    ) -> str:
        """Focus the session on a specific pending task.

        Loads the task board and relevant context for starting or resuming work
        on a task. Call this at the beginning of a focused work session.

        Args:
            task_name: Name or keyword of the task to focus on. If omitted,
                       shows all pending and in-progress tasks to choose from.
        """
        if _dir is None:
            return _NO_PROJECT_MSG

        status_path = _dir / "project" / "status.md"
        if not status_path.exists():
            return "project/status.md not found. Run `co scaffold` to create it."

        status_content = status_path.read_text(encoding="utf-8")

        parts = ["# Start Task\n"]

        if task_name:
            parts.append(f"**Focusing on task:** {task_name}\n")
            parts.append(
                "Review the task board below, locate this task, and confirm your plan "
                "before making any changes. Update its status in status.md once you begin.\n"
            )
        else:
            parts.append(
                "No specific task provided. Review the task board below and identify "
                "which task to work on. Confirm your choice before starting.\n"
            )

        parts.append(f"## project/status.md\n\n{status_content}\n")

        # Also load core rules as a reminder
        rules_path = _dir / "core" / "core-rules.md"
        if rules_path.exists():
            parts.append(
                "---\n\n"
                "## Reminder: Core Rules\n\n"
                + rules_path.read_text(encoding="utf-8")
                + "\n"
            )

        parts.append(
            "\n---\n"
            "When you begin working: call `update_status` to mark the task as in-progress. "
            "When done: call `update_status` to mark it complete and record session context."
        )

        return "\n".join(parts)

    @mcp.prompt()
    def debug_session(
        symptom: str | None = None,
    ) -> str:
        """Set up a structured debugging session with full project context.

        Loads known issues, tech stack, and status to provide grounding for
        systematic bug investigation.

        Args:
            symptom: Brief description of the bug or unexpected behavior. Optional.
        """
        if _dir is None:
            return _NO_PROJECT_MSG

        parts = ["# Debug Session\n"]

        if symptom:
            parts.append(f"**Symptom reported:** {symptom}\n")
        else:
            parts.append("No symptom provided — describe the unexpected behavior to begin.\n")

        parts.append(
            "Follow this systematic approach:\n"
            "1. Reproduce the issue with a minimal test case.\n"
            "2. Check known-issues.md first — the bug may already be documented.\n"
            "3. Gather runtime evidence (logs, stack traces, actual vs expected output).\n"
            "4. Identify the root cause before proposing a fix.\n"
            "5. Verify the fix does not regress existing tests.\n"
            "6. Update known-issues.md if this reveals a new pattern.\n\n"
        )

        for filename in ["project/known-issues.md", "project/tech-stack.md", "project/status.md"]:
            fpath = _dir / filename
            if fpath.exists():
                parts.append(f"## {filename}\n\n{fpath.read_text(encoding='utf-8')}\n")

        parts.append(
            "\n---\n"
            "After resolving the bug: call `update_status` to record what was found and fixed. "
            "Add to known-issues.md if this is a pattern worth documenting."
        )

        return "\n".join(parts)

    @mcp.prompt()
    def pre_commit() -> str:
        """Prepare for a git commit: review changes and update project status.

        Loads the current task board and session context to help generate an
        accurate commit message and ensure status.md is up to date before committing.
        """
        if _dir is None:
            return _NO_PROJECT_MSG

        parts = ["# Pre-Commit Checklist\n"]

        parts.append(
            "Before committing, complete the following:\n\n"
            "1. **Review staged changes** — confirm they match the intended task scope.\n"
            "2. **Update status.md** — call `update_status` to mark completed tasks and "
            "record session context. Do this BEFORE running `git commit`.\n"
            "3. **Write commit message** — be specific: what changed and why "
            "(not just 'fix bug' or 'update code').\n"
            "4. **Check delivery quality**:\n"
            "   - UTF-8 encoding declared where files are read/written\n"
            "   - No hardcoded version numbers duplicated across files\n"
            "   - No external CDN links introduced\n"
            "   - Test data uses `autotest_` prefix\n"
            "5. **Run lint** — use `lint_protocol` tool or `co lint` to check .agent/ compliance.\n\n"
        )

        status_path = _dir / "project" / "status.md"
        if status_path.exists():
            parts.append(f"## Current Status (project/status.md)\n\n{status_path.read_text(encoding='utf-8')}\n")

        parts.append(
            "\n---\n"
            "Call `update_status` now with:\n"
            "- `completed_tasks`: list what you finished in this session\n"
            "- `session_context`: key info for the next session\n"
            "Then proceed with `git add` and `git commit`."
        )

        return "\n".join(parts)

    @mcp.prompt()
    def release_prep() -> str:
        """Prepare for a release: checklist and status review.

        Loads the current status and active goals to help verify release
        readiness and generate release notes.
        """
        if _dir is None:
            return _NO_PROJECT_MSG

        parts = ["# Release Preparation\n"]

        parts.append(
            "Before tagging a release, verify the following:\n\n"
            "1. **Version bump** — update version in `pyproject.toml` (or equivalent). "
            "This is the single source of truth; do not duplicate in other files.\n"
            "2. **RELEASE.md / CHANGELOG** — document what changed since the last tag.\n"
            "3. **All pending tasks resolved** — no in-progress items left on the task board.\n"
            "4. **Tests passing** — run the full test suite; no regressions.\n"
            "5. **protocol lint clean** — run `lint_protocol` or `co lint`; 0 errors.\n"
            "6. **status.md updated** — phase reflects the new release; "
            "Recently Completed includes this release.\n"
            "7. **Git tag** — create an annotated tag: `git tag -a vX.Y.Z -m 'vX.Y.Z'`.\n"
            "8. **Push** — `git push && git push --tags`.\n\n"
        )

        for filename in ["project/status.md"]:
            fpath = _dir / filename
            if fpath.exists():
                parts.append(f"## {filename}\n\n{fpath.read_text(encoding='utf-8')}\n")

        parts.append(
            "\n---\n"
            "After tagging: call `update_status` with the new phase description "
            "and move release task to Recently Completed."
        )

        return "\n".join(parts)

    return mcp


def _register_global_tools(mcp: FastMCP) -> None:
    """Register the four global-registry MCP tools onto any FastMCP instance.

    These tools let the AI query **any** cokodo project registered on this
    machine, regardless of which project the MCP server was launched from.
    Called from both create_server() and create_global_server().
    """

    @mcp.tool()
    def list_global_projects(include_invalid: bool = False) -> str:
        """List all cokodo projects registered on this machine.

        Projects are registered automatically each time any `co` command is
        run inside a directory that has a .agent/ directory.

        Args:
            include_invalid: If True, also show projects whose paths no
                             longer exist on disk. Default False.

        Returns:
            Formatted list of all registered projects with name, path,
            tech stack, last-seen timestamp, and status summary.
        """
        from cokodo_agent.registry import list_projects

        projects = list_projects(include_invalid=include_invalid)
        if not projects:
            return (
                "No projects registered yet.\n\n"
                "Projects register automatically when you run any `co` command "
                "inside a directory that has .agent/."
            )

        lines: list[str] = [f"# Registered Projects ({len(projects)} total)\n"]
        for p in projects:
            valid_flag = "" if p.get("is_valid") else " [INVALID]"
            lines.append(f"## {p['name']}{valid_flag}")
            lines.append(f"- id: {p['id']}")
            lines.append(f"- path: {p['path']}")
            if p.get("stack"):
                lines.append(f"- stack: {p['stack']}")
            if p.get("protocol_version"):
                lines.append(f"- protocol: {p['protocol_version']}")
            if p.get("last_seen"):
                lines.append(f"- last_seen: {p['last_seen'][:10]}")
            if p.get("status_summary"):
                lines.append(f"- status: {p['status_summary']}")
            lines.append("")

        return "\n".join(lines)

    @mcp.tool()
    def get_global_project_context(
        project: str,
        files: list[str] | None = None,
    ) -> str:
        """Get context files from any registered project by name or ID.

        Use list_global_projects first to discover available projects and
        their names / IDs.

        Args:
            project: Project name or ID (prefix of 4+ chars accepted).
            files: Optional list of specific relative paths to read from the
                   project's .agent/ directory (e.g. ["project/status.md",
                   "project/context.md"]). If omitted, reads the standard
                   set: status, context, tech-stack, commands, known-issues.

        Returns:
            Concatenated contents of the requested .agent/ files, prefixed
            with the project name and source path for clarity.
        """
        from cokodo_agent.registry import get_project, get_project_context_files

        proj = get_project(project)
        if proj is None:
            return (
                f"Project '{project}' not found in global registry.\n"
                "Use list_global_projects to see all registered projects."
            )

        if not proj.get("is_valid"):
            return (
                f"Project '{proj['name']}' path no longer exists: {proj['path']}\n"
                "Run `co global gc` to clean up stale registry entries."
            )

        agent_dir = Path(proj["path"]) / ".agent"
        if not agent_dir.is_dir():
            return (
                f"Project '{proj['name']}' has no .agent/ directory at {proj['path']}.\n"
                "The project may have been moved or deleted."
            )

        if files:
            # Read specific requested files
            content_map: dict[str, str] = {}
            for rel in files:
                safe = Path(rel)
                if ".." in safe.parts:
                    continue
                fpath = agent_dir / safe
                if fpath.exists():
                    try:
                        content_map[rel] = fpath.read_text(encoding="utf-8")
                    except OSError:
                        pass
        else:
            content_map = get_project_context_files(proj)

        if not content_map:
            return f"No context files found for project '{proj['name']}'."

        parts: list[str] = [
            f"# Project: {proj['name']}\n"
            f"Path: {proj['path']}\n"
        ]
        for rel_path, content in content_map.items():
            parts.append(f"## {rel_path}\n\n{content}")
        return "\n---\n\n".join(parts)

    @mcp.tool()
    def get_global_project_status(project: str) -> str:
        """Get the current development status of any registered project.

        Reads project/status.md directly from the project's .agent/ directory,
        always returning the live file content (not a cached snapshot).

        Args:
            project: Project name or ID (prefix of 4+ chars accepted).

        Returns:
            Full contents of project/status.md, or an error message.
        """
        from cokodo_agent.registry import get_project

        proj = get_project(project)
        if proj is None:
            return (
                f"Project '{project}' not found in global registry.\n"
                "Use list_global_projects to see all registered projects."
            )

        status_file = Path(proj["path"]) / ".agent" / "project" / "status.md"
        if not status_file.exists():
            return (
                f"No status.md found for project '{proj['name']}' "
                f"at {proj['path']}/.agent/project/status.md"
            )

        try:
            return status_file.read_text(encoding="utf-8")
        except OSError as e:
            return f"Error reading status for '{proj['name']}': {e}"

    @mcp.tool()
    def global_search(
        keyword: str,
        include_file_content: bool = False,
    ) -> str:
        """Search across all registered projects by keyword.

        Searches project names, status summaries, and file paths.
        Optionally also scans the content of key .agent/ files.

        Args:
            keyword: Search term (case-insensitive).
            include_file_content: If True, also search inside project/status.md
                                  and project/context.md files. Slower but more
                                  thorough. Default False.

        Returns:
            List of matching projects with match context, or "No matches found."
        """
        from cokodo_agent.registry import search_projects

        results = search_projects(keyword)

        # Optionally deep-search file content for projects not already matched
        if include_file_content:
            from cokodo_agent.registry import list_projects

            all_ids = {p["id"] for p in results}
            kw_lower = keyword.lower()
            for p in list_projects():
                if p["id"] in all_ids:
                    continue
                agent_dir = Path(p["path"]) / ".agent"
                for rel in ("project/status.md", "project/context.md"):
                    fpath = agent_dir / rel
                    if fpath.exists():
                        try:
                            if kw_lower in fpath.read_text(encoding="utf-8").lower():
                                results.append(p)
                                all_ids.add(p["id"])
                                break
                        except OSError:
                            pass

        if not results:
            return f"No projects matching '{keyword}'."

        lines: list[str] = [
            f"# Search Results for '{keyword}' ({len(results)} match(es))\n"
        ]
        for p in results:
            lines.append(f"## {p['name']}")
            lines.append(f"- path: {p['path']}")
            if p.get("stack"):
                lines.append(f"- stack: {p['stack']}")
            if p.get("status_summary"):
                lines.append(f"- status: {p['status_summary']}")
            lines.append(f"- last_seen: {(p.get('last_seen') or '')[:10]}")
            lines.append("")

        return "\n".join(lines)


def _find_agent_dir() -> Path:
    """Search for .agent/ directory from cwd upward."""
    current = Path.cwd()
    for parent in [current, *current.parents]:
        candidate = parent / ".agent"
        if candidate.is_dir() and (candidate / "manifest.json").exists():
            return candidate
    raise FileNotFoundError(
        "No .agent/ directory found. Run 'co init' to create one."
    )


# ---------------------------------------------------------------------------
# Workspace mode: multi-project discovery
# ---------------------------------------------------------------------------


def discover_agent_dirs(workspace_root: Path, max_depth: int = 2) -> dict[str, Path]:
    """Discover all .agent/ directories under workspace_root.

    Returns a dict mapping project name (directory name) -> .agent/ path.
    Only scans up to max_depth levels deep.
    """
    results: dict[str, Path] = {}

    def _scan(current: Path, depth: int) -> None:
        if depth > max_depth:
            return
        try:
            for child in sorted(current.iterdir()):
                if not child.is_dir():
                    continue
                if child.name.startswith(".") and child.name != ".agent":
                    continue
                if child.name in ("node_modules", "__pycache__", ".git", "venv", ".venv"):
                    continue
                agent_candidate = child / ".agent"
                if (
                    agent_candidate.is_dir()
                    and (agent_candidate / "manifest.json").exists()
                ):
                    project_name = child.name
                    results[project_name] = agent_candidate
                else:
                    _scan(child, depth + 1)
        except PermissionError:
            pass

    # Also check the workspace root itself
    root_agent = workspace_root / ".agent"
    if root_agent.is_dir() and (root_agent / "manifest.json").exists():
        results[workspace_root.name] = root_agent

    _scan(workspace_root, 0)
    return results


def create_workspace_server(
    workspace_root: Path,
    transport: str = "stdio",
) -> FastMCP:
    """Create a workspace-aware MCP server that exposes multiple projects.

    Discovers all .agent/ directories under workspace_root and provides
    tools to list projects and query context from any of them.
    """
    projects = discover_agent_dirs(workspace_root)
    _ws_root = workspace_root

    mcp = FastMCP(
        "cokodo-workspace",
        instructions=(
            "This server provides access to multiple AI Agent Collaboration Protocol "
            "projects in a workspace. Use list_workspace_projects first to discover "
            "available projects, then use workspace_get_context to load context "
            "from a specific project."
        ),
    )

    @mcp.tool()
    def list_workspace_projects() -> str:
        """List all projects discovered in the workspace.

        Returns project names, paths, and basic status.
        """
        if not projects:
            return "No projects with .agent/ found in workspace."

        lines: list[str] = [f"# Workspace: {_ws_root.name}\n"]
        for name, agent_path in sorted(projects.items()):
            project_root = agent_path.parent
            status_file = agent_path / "project" / "status.md"
            has_status = status_file.exists()
            lines.append(f"- **{name}**: {project_root}")
            lines.append(f"  status.md: {'yes' if has_status else 'no'}")

            from cokodo_agent.relations import load_collaborations, load_references

            refs = load_references(agent_path)
            collabs = load_collaborations(agent_path)
            if refs:
                lines.append(f"  references: {len(refs)}")
            if collabs:
                lines.append(f"  collaborations: {len(collabs)}")
            lines.append("")

        return "\n".join(lines)

    @mcp.tool()
    def workspace_get_context(
        project: str,
        stack: str | None = None,
        task: str | None = None,
    ) -> str:
        """Get context files from a specific project in the workspace.

        Args:
            project: Project name (from list_workspace_projects).
            stack: Tech stack filter (python/rust/qt/mixed). Optional.
            task: Task type or profile. Optional.

        Returns:
            Concatenated contents of all relevant .agent/ files.
        """
        if project not in projects:
            available = ", ".join(sorted(projects.keys()))
            return (
                f"Project '{project}' not found. "
                f"Available projects: {available}"
            )

        agent_dir = projects[project]
        files = get_context_files(agent_dir, stack=stack, task=task)
        if not files:
            return f"No context files found for project '{project}'."

        content_map = build_context_content(agent_dir, files)
        if not content_map:
            return f"No context files could be read for project '{project}'."

        parts: list[str] = [f"# Project: {project}\n"]
        for rel_path, content in content_map.items():
            parts.append(f"## {rel_path}\n\n{content}")
        return "\n---\n\n".join(parts)

    @mcp.tool()
    def workspace_get_status(project: str) -> str:
        """Get the current development status of a specific project.

        Args:
            project: Project name (from list_workspace_projects).

        Returns:
            Contents of project/status.md, or error message.
        """
        if project not in projects:
            available = ", ".join(sorted(projects.keys()))
            return f"Project '{project}' not found. Available: {available}"

        status_file = projects[project] / "project" / "status.md"
        if not status_file.exists():
            return f"No status.md found for project '{project}'."

        return status_file.read_text(encoding="utf-8")

    @mcp.tool()
    def workspace_read_file(project: str, path: str) -> str:
        """Read a file from a specific project's .agent/ directory.

        Args:
            project: Project name (from list_workspace_projects).
            path: Relative path within the project's .agent/ directory.

        Returns:
            File contents, or error message.
        """
        if project not in projects:
            available = ", ".join(sorted(projects.keys()))
            return f"Project '{project}' not found. Available: {available}"

        safe_path = Path(path)
        if ".." in safe_path.parts:
            return "Error: path traversal not allowed"

        full = projects[project] / safe_path
        if not full.exists():
            return f"File not found: {path} in project '{project}'"

        try:
            return full.read_text(encoding="utf-8")
        except OSError as e:
            return f"Error reading {path}: {e}"

    @mcp.tool()
    def workspace_list_relations(project: str | None = None) -> str:
        """List cross-project relationships for one or all workspace projects.

        Args:
            project: Specific project name. If omitted, shows all projects.

        Returns:
            Summary of references and collaborations.
        """
        target_projects = (
            {project: projects[project]}
            if project and project in projects
            else projects
        )

        if project and project not in projects:
            available = ", ".join(sorted(projects.keys()))
            return f"Project '{project}' not found. Available: {available}"

        from cokodo_agent.relations import load_collaborations, load_references

        lines: list[str] = []
        for name, agent_dir in sorted(target_projects.items()):
            refs = load_references(agent_dir)
            collabs = load_collaborations(agent_dir)
            if not refs and not collabs:
                continue

            lines.append(f"## {name}")
            for r in refs:
                lines.append(f"  ref: {r.name} -> {r.source}")
            for c in collabs:
                lines.append(f"  collab: {c.name} ({c.role}) -> {c.partner}")
            lines.append("")

        if not lines:
            return "No cross-project relationships found in workspace."

        return "\n".join(lines)

    @mcp.tool()
    def workspace_health_check() -> str:
        """Run health check on all projects and their relationships.

        Returns a workspace-wide summary of reference and collaboration health.
        """
        from cokodo_agent.relations import (
            check_collaborations,
            check_references,
            load_collaborations,
            load_references,
        )

        lines: list[str] = [f"# Workspace Health: {_ws_root.name}\n"]
        total_ok = 0
        total_fail = 0

        for name, agent_dir in sorted(projects.items()):
            project_root = agent_dir.parent
            refs = load_references(agent_dir)
            collabs = load_collaborations(agent_dir)

            if not refs and not collabs:
                lines.append(f"## {name}: no relationships")
                lines.append("")
                continue

            lines.append(f"## {name}")
            if refs:
                for st in check_references(agent_dir, project_root):
                    if st.accessible:
                        lines.append(f"  [OK] ref:{st.name}")
                        total_ok += 1
                    else:
                        lines.append(f"  [FAIL] ref:{st.name}: {st.error}")
                        total_fail += 1

            if collabs:
                for cst in check_collaborations(agent_dir, project_root):
                    if cst.partner_accessible:
                        sync = "in_sync" if cst.shared_files_in_sync else "drifted"
                        lines.append(f"  [OK] collab:{cst.name} ({sync})")
                        total_ok += 1
                    else:
                        lines.append(f"  [FAIL] collab:{cst.name}: {cst.error}")
                        total_fail += 1
            lines.append("")

        lines.append(
            f"\nTotal: {total_ok}/{total_ok + total_fail} relationships healthy "
            f"across {len(projects)} project(s)."
        )
        return "\n".join(lines)

    # Global registry tools also available in workspace mode
    _register_global_tools(mcp)

    return mcp


def create_global_server() -> FastMCP:
    """Create a global MCP server backed only by the local project registry.

    This server exposes the four global-registry tools
    (list_global_projects, get_global_project_context,
    get_global_project_status, global_search) without requiring a
    specific .agent/ directory. Useful when the MCP client is NOT
    launched from inside a project directory.

    Example::

        co serve --global          # stdio transport
        co serve --global --sse    # SSE transport
    """
    mcp = FastMCP(
        "cokodo-global",
        instructions=(
            "This server provides access to ALL cokodo projects registered on this "
            "machine via the global project registry (~/.cokodo/registry.db). "
            "Use list_global_projects to discover projects, then "
            "get_global_project_context or get_global_project_status to load "
            "details from any specific project."
        ),
    )

    _register_global_tools(mcp)

    return mcp


def main(transport: str = "stdio") -> None:
    """Entry point for the MCP server."""
    server = create_server()
    server.run(transport=transport)
