"""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}")
        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.prompt()
    def session_init(
        stack: str | None = None,
        task: str | None = None,
    ) -> str:
        """Build a complete session initialization prompt with all relevant project context.

        Args:
            stack: Tech stack filter (python/rust/qt/mixed). Optional.
            task: Task type or profile. Optional.
        """
        if _dir is None:
            return _NO_PROJECT_MSG

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

        parts: list[str] = [
            "# Session Context\n",
            "The following project context has been loaded from .agent/ protocol.\n",
        ]

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

        parts.append(
            "\n---\n"
            "You now have the full project context. "
            "After completing tasks, call update_status to record progress."
        )
        return "\n".join(parts)

    return mcp


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."
    )


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