"""Server construction and dispatch.

`build_server` wires MCP's `list_tools` / `call_tool` decorators to
handler functions living in `.handlers.*`. Each handler accepts
`ctx: ServerContext` as its first parameter; dispatch passes `ctx`
explicitly.
"""
from __future__ import annotations

from pathlib import Path

from mcp.server import Server
from mcp.types import TextContent, Tool

# Late binding — see helpers.py docstring. `build_server` reads
# exception classes (GitHubError, ClassificationError,
# InvalidNodeIdError) via _s so tests can monkey-patch them if needed,
# matching pre-split behaviour.
from active_claude_github_mcp import server as _s
from ..buffer import write_pending
from ..config import get_github_token, load_config, require_env
from ..context import ServerContext
from ..models import GithubConfig, OperationError
from ..parsers import parse_owner_repo
from .helpers import (
    err,
    ok,
    preflight_cross_repo_mutation,
    resolve_thread_identifier,
    validate_tool_node_ids,
)
from .preflight import CrossRepoMutationBlocked
from .handlers import infra as infra_handlers
from .handlers import issues as issues_handlers
from .handlers import meta as meta_handlers
from .handlers import release as release_handlers
from .handlers import session as session_handlers
from .handlers import threads as threads_handlers
from .tool_schemas import all_tool_schemas


def build_server(project_dir: Path) -> tuple[Server, GithubConfig, str, str]:
    """Build and configure the MCP server.

    Fails immediately (ConfigError) if config is invalid, ANTHROPIC_API_KEY
    is unset, or a GitHub token is unavailable.

    Returns (server, config, github_token, anthropic_api_key).
    """
    config = load_config(project_dir)
    github_token = get_github_token()
    anthropic_api_key = require_env("ANTHROPIC_API_KEY")

    server = Server("active-claude-github")

    ctx = ServerContext(
        config=config,
        github_token=github_token,
        anthropic_api_key=anthropic_api_key,
        project_dir=project_dir,
    )

    @server.list_tools()
    async def list_tools() -> list[Tool]:
        return all_tool_schemas()

    @server.call_tool()
    async def call_tool(name: str, arguments: dict) -> list[TextContent]:
        owner, private_repo = parse_owner_repo(ctx.config.repos.private)

        try:
            validate_tool_node_ids(name, arguments, ctx.github_token)

            # Cross-repo mutation preflight (#98). No-op unless the
            # caller passed explicit owner/repo targeting a cousin /
            # foreign repo per related_repos.
            preflight_cross_repo_mutation(ctx.config, arguments)

            if name == "open_session":
                return await session_handlers.open_session(ctx)

            if name == "recover_context":
                return await session_handlers.recover_context(
                    ctx, int(arguments.get("days", 7))
                )

            if name == "scaffold_project":
                return await infra_handlers.scaffold_project(
                    ctx,
                    arguments["org"],
                    arguments["project_name"],
                    arguments.get("description", ""),
                )

            if name == "classify_and_create":
                return await meta_handlers.classify_and_create(
                    ctx,
                    arguments["text"],
                    arguments.get("auto_create_threshold", "high"),
                    bool(arguments.get("also_post_turn", True)),
                    arguments.get("opening_body", ""),
                    owner,
                    private_repo,
                )

            if name == "check_skill_version":
                return await infra_handlers.check_skill_version(ctx)

            if name == "replay_pending":
                return await infra_handlers.replay_pending(ctx, call_tool)

            if name == "close_issue_with_comment":
                issue_id = resolve_thread_identifier(
                    ctx.github_token,
                    arguments,
                    primary_id_param="issue_id",
                    kind="issue",
                    default_owner=owner,
                    default_repo=private_repo,
                )
                return await issues_handlers.close_issue_with_comment(
                    ctx,
                    issue_id,
                    arguments["comment"],
                    arguments.get("state_reason", "completed"),
                )

            if name == "get_thread_comments":
                return await threads_handlers.get_thread_comments(
                    ctx,
                    arguments["thread_id"],
                    arguments["thread_type"],
                    int(arguments.get("limit", 20)),
                )

            if name == "close_session":
                return await session_handlers.close_session(
                    ctx,
                    arguments.get("summary"),
                    int(arguments.get("skill_update_candidates", 0)),
                )

            if name == "post_turn":
                thread_type = arguments["thread_type"]
                thread_id = resolve_thread_identifier(
                    ctx.github_token,
                    arguments,
                    primary_id_param="thread_id",
                    kind=thread_type,
                    default_owner=owner,
                    default_repo=private_repo,
                )
                return await threads_handlers.post_turn(
                    ctx,
                    thread_id,
                    thread_type,
                    arguments["speaker"],
                    arguments["body"],
                    ctx.project_dir,
                )

            if name == "create_thread":
                return await threads_handlers.create_thread(
                    ctx,
                    arguments["type"],
                    arguments["category_or_label"],
                    arguments["title"],
                    arguments["opening_body"],
                    arguments.get("discussion_origin_id", ""),
                    owner,
                    private_repo,
                )

            if name == "classify_turn":
                return await meta_handlers.classify_turn(ctx, arguments["text"])

            if name == "detect_theme_change":
                return await meta_handlers.detect_theme_change(
                    ctx,
                    arguments["thread_id"],
                    arguments["new_turn_text"],
                )

            if name == "fork_discussion":
                original_id = resolve_thread_identifier(
                    ctx.github_token,
                    arguments,
                    primary_id_param="original_id",
                    kind="discussion",
                    default_owner=owner,
                    default_repo=private_repo,
                )
                fork_args = dict(arguments)
                fork_args["original_id"] = original_id
                return await threads_handlers.fork_discussion(
                    ctx, fork_args, owner, private_repo
                )

            if name == "create_issue":
                # Accept either `labels: List[str]` (#55) or the deprecated
                # single `label: str` form. Exactly one must be provided.
                raw_labels = arguments.get("labels")
                legacy_label = arguments.get("label")
                if raw_labels is not None and legacy_label:
                    raise ValueError(
                        "Provide either `labels` or `label`, not both. "
                        "`label` is deprecated."
                    )
                if raw_labels is None and not legacy_label:
                    raise ValueError(
                        "Either `labels` (list) or `label` (deprecated "
                        "single value) must be provided."
                    )
                labels_list = (
                    [legacy_label] if legacy_label else list(raw_labels or [])
                )
                return await issues_handlers.create_issue_tool(
                    ctx,
                    arguments["title"],
                    arguments["body"],
                    labels_list,
                    arguments.get("discussion_origin_id", ""),
                    owner,
                    private_repo,
                )

            if name == "update_issue_toc":
                issue_id = resolve_thread_identifier(
                    ctx.github_token,
                    arguments,
                    primary_id_param="issue_id",
                    kind="issue",
                    default_owner=owner,
                    default_repo=private_repo,
                )
                return await issues_handlers.update_issue_toc(
                    ctx,
                    issue_id,
                    arguments["new_body"],
                    bool(arguments.get("force", False)),
                )

            if name == "move_project_item":
                issue_id = resolve_thread_identifier(
                    ctx.github_token,
                    arguments,
                    primary_id_param="issue_id",
                    kind="issue",
                    default_owner=owner,
                    default_repo=private_repo,
                )
                return await issues_handlers.move_project_item(
                    ctx,
                    arguments["column_name"],
                    issue_id,
                )

            if name == "update_project_toc":
                return await threads_handlers.update_project_toc(
                    ctx,
                    arguments["new_body"],
                    bool(arguments.get("force", False)),
                )

            if name == "generate_changelog":
                return await release_handlers.generate_changelog(
                    ctx,
                    bool(arguments.get("write", False)),
                )

            if name == "validate_release":
                return await release_handlers.validate_release(
                    ctx,
                    arguments["version"],
                    bool(arguments.get("skip_bazel", False)),
                )

            if name == "release_project":
                return await release_handlers.release_project(
                    ctx,
                    arguments["version"],
                    bool(arguments.get("dry_run", True)),
                    arguments.get("stop_after", "release_notes"),
                    bool(arguments.get("approve_version_bump", False)),
                    bool(arguments.get("skip_bazel", False)),
                )

            if name == "mark_discussion_decided":
                discussion_id = resolve_thread_identifier(
                    ctx.github_token,
                    arguments,
                    primary_id_param="discussion_id",
                    kind="discussion",
                    default_owner=owner,
                    default_repo=private_repo,
                )
                return await threads_handlers.mark_decided(
                    ctx,
                    discussion_id,
                    arguments["decision_comment_id"],
                    arguments["category_name"],
                )

            return ok(f"Unknown tool: {name}")

        except _s.InvalidNodeIdError as exc:
            # Client-side error — do NOT buffer. Retrying the same bad
            # ID would fail identically and clog the buffer (#51).
            ctx.activity.tool_errors += 1
            return err(
                OperationError(
                    success=False,
                    error=str(exc),
                    buffered=False,
                    buffer_file=None,
                    retry_tool=None,
                    retry_args=None,
                )
            )

        except CrossRepoMutationBlocked as exc:
            # Protocol gate, not a transient error (#98). Not buffered —
            # retrying without user permission would fail identically.
            ctx.activity.tool_errors += 1
            return err(
                OperationError(
                    success=False,
                    error=(
                        f"{exc.relationship}-repo mutation blocked: "
                        f"{exc.target_repo}"
                    ),
                    buffered=False,
                    buffer_file=None,
                    retry_tool=None,
                    retry_args=None,
                    manual_step=exc.manual_step,
                )
            )

        except (ValueError, _s.GitHubError, _s.ClassificationError) as exc:
            ctx.activity.tool_errors += 1
            buffer_path = write_pending(
                ctx.project_dir, name, arguments, str(exc)
            )
            return err(
                OperationError(
                    success=False,
                    error=str(exc),
                    buffered=True,
                    buffer_file=str(buffer_path),
                    retry_tool=name,
                    retry_args=arguments,
                )
            )

    # Expose the registered handlers for test harnesses (#65).
    server.list_tools_handler = list_tools  # type: ignore[attr-defined]
    server.call_tool_handler = call_tool  # type: ignore[attr-defined]

    return server, config, github_token, anthropic_api_key
