"""ACC C2C MCP Server — exposes ACC C2C external API as MCP tools.

Configuration (env vars):
    ACC_BASE_URL      — ACC API base, e.g. https://clawweb.eastasia.cloudapp.azure.com/api/v1
    ACC_API_KEY       — API key for this claw (acc_sk_...)
    ACC_VERIFY_SSL    — Set to "false" to skip SSL verification (default: true)
"""

from __future__ import annotations

import json
import os
from typing import Any

import httpx
from mcp.server.fastmcp import FastMCP

from .client import ACCClient, ACCError

mcp = FastMCP(
    "ACC C2C",
    instructions=(
        "ACC Claw-to-Claw (C2C) MCP server. Provides tools for dispatching "
        "tasks between claws in an ACC pipeline.\n\n"
        "Key concept: each dispatch includes a `routingContext` field — a "
        "snapshot of the pipeline topology at creation time. Read it to know "
        "your upstream, downstream neighbors, and their skills. No need to "
        "cache global state.\n\n"
        "Call the `c2c_onboard` prompt for a full C2C workflow guide and your "
        "current pipeline context."
    ),
)

_client: ACCClient | None = None


def _get_client() -> ACCClient:
    global _client
    if _client is not None:
        return _client

    base = os.environ.get("ACC_BASE_URL", "")
    key = os.environ.get("ACC_API_KEY", "")
    if not base or not key:
        raise RuntimeError(
            "Set ACC_BASE_URL and ACC_API_KEY environment variables. "
            "Example: ACC_BASE_URL=https://clawweb.eastasia.cloudapp.azure.com/api/v1 "
            "ACC_API_KEY=acc_sk_..."
        )
    verify = os.environ.get("ACC_VERIFY_SSL", "true").lower() not in ("false", "0", "no")
    _client = ACCClient(base, key, verify_ssl=verify)
    return _client


def _pretty(data: Any) -> str:
    return json.dumps(data, indent=2, ensure_ascii=False)


def _parse_json_arg(value: str | None, name: str) -> dict | None:
    if not value:
        return None
    try:
        parsed = json.loads(value)
    except json.JSONDecodeError as e:
        raise ValueError(f"Invalid JSON for '{name}': {e}")
    if not isinstance(parsed, dict):
        raise ValueError(f"'{name}' must be a JSON object, got {type(parsed).__name__}")
    return parsed


async def _call(coro):
    """Wrap API calls to return structured error messages instead of tracebacks."""
    try:
        return await coro
    except ACCError as e:
        return {"error": True, "status": e.status, "detail": e.detail}
    except httpx.HTTPError as e:
        return {"error": True, "detail": f"Network error: {e}"}
    except (ValueError, KeyError, TypeError, RuntimeError) as e:
        return {"error": True, "detail": str(e)}


# ── Protocol ──


@mcp.tool()
async def acc_get_protocol(pipeline_id: str | None = None) -> str:
    """Get the C2C protocol for this claw.

    Returns a scoped view showing your claw's position, upstream/downstream
    neighbors, connection types, available API endpoints, and the dispatch
    lifecycle state machine.

    Args:
        pipeline_id: Optional pipeline ID. If omitted and the claw belongs
                     to exactly one pipeline, that pipeline is used automatically.
    """
    async def _do():
        return await _get_client().get_protocol(pipeline_id)

    result = await _call(_do())
    return _pretty(result)


# ── Dispatch ──


@mcp.tool()
async def acc_dispatch(
    pipeline_id: str,
    target_claw_id: str,
    title: str,
    objective: str = "",
    priority: str = "p2",
    context: str | None = None,
    due_at: str | None = None,
    parent_dispatch_id: str | None = None,
) -> str:
    """Create a C2C dispatch (task) to another claw in the pipeline.

    The target claw must be a direct downstream (or bidirectional) neighbor.

    Args:
        pipeline_id: Pipeline ID.
        target_claw_id: ID of the claw to dispatch to.
        title: Short title for the task.
        objective: Detailed description of what needs to be done.
        priority: p0 (critical) / p1 (high) / p2 (normal) / p3 (low).
        context: Optional JSON string with additional context.
        due_at: Optional deadline in ISO 8601 format.
        parent_dispatch_id: Optional parent dispatch ID for sub-tasks.
    """
    async def _do():
        ctx = _parse_json_arg(context, "context")
        body: dict[str, Any] = {
            "pipelineId": pipeline_id,
            "targetClawId": target_claw_id,
            "title": title,
            "objective": objective,
            "priority": priority,
        }
        if ctx:
            body["context"] = ctx
        if due_at:
            body["dueAt"] = due_at
        if parent_dispatch_id:
            body["parentDispatchId"] = parent_dispatch_id
        return await _get_client().create_dispatch(body)

    result = await _call(_do())
    return _pretty(result)


@mcp.tool()
async def acc_list_dispatches(
    pipeline_id: str | None = None,
    status: str | None = None,
    page: int = 1,
    page_size: int = 20,
) -> str:
    """List dispatches involving this claw (as source or target).

    Args:
        pipeline_id: Filter by pipeline.
        status: Filter by status (pending/accepted/in_progress/completed/...).
        page: Page number (1-based).
        page_size: Items per page (max 100).
    """
    async def _do():
        return await _get_client().list_dispatches(
            pipeline_id=pipeline_id, status=status,
            page=page, page_size=page_size,
        )

    result = await _call(_do())
    return _pretty(result)


@mcp.tool()
async def acc_get_dispatch(dispatch_id: str) -> str:
    """Get details of a specific dispatch.

    Args:
        dispatch_id: The dispatch ID.
    """
    async def _do():
        return await _get_client().get_dispatch(dispatch_id)

    result = await _call(_do())
    return _pretty(result)


# ── Lifecycle ──


@mcp.tool()
async def acc_accept_dispatch(
    dispatch_id: str,
    message: str | None = None,
    estimated_completion: str | None = None,
) -> str:
    """Accept a pending dispatch assigned to this claw.

    Args:
        dispatch_id: The dispatch ID to accept.
        message: Optional acceptance message.
        estimated_completion: Optional ETA string.
    """
    async def _do():
        return await _get_client().accept(dispatch_id, message=message, eta=estimated_completion)

    result = await _call(_do())
    return _pretty(result)


@mcp.tool()
async def acc_reject_dispatch(
    dispatch_id: str,
    reason: str,
    suggest_target: str | None = None,
) -> str:
    """Reject a pending dispatch with a reason.

    Args:
        dispatch_id: The dispatch ID to reject.
        reason: Why this dispatch is being rejected.
        suggest_target: Optional alternative claw ID to redirect to.
    """
    async def _do():
        return await _get_client().reject(dispatch_id, reason, suggest_target=suggest_target)

    result = await _call(_do())
    return _pretty(result)


@mcp.tool()
async def acc_report_progress(
    dispatch_id: str,
    content: str,
    report_type: str = "progress",
    progress_pct: int | None = None,
) -> str:
    """Submit a progress report on an active dispatch.

    Args:
        dispatch_id: The dispatch ID.
        content: Report content / status update.
        report_type: progress / checkpoint / blocker / question.
        progress_pct: Optional completion percentage (0-100).
    """
    async def _do():
        return await _get_client().report(
            dispatch_id, report_type=report_type,
            content=content, progress_pct=progress_pct,
        )

    result = await _call(_do())
    return _pretty(result)


@mcp.tool()
async def acc_complete_dispatch(
    dispatch_id: str,
    content: str = "",
    artifacts: str | None = None,
) -> str:
    """Mark a dispatch as completed with results.

    Args:
        dispatch_id: The dispatch ID.
        content: Result summary.
        artifacts: Optional JSON string with result artifacts.
    """
    async def _do():
        arts = _parse_json_arg(artifacts, "artifacts")
        return await _get_client().complete(dispatch_id, content, artifacts=arts)

    result = await _call(_do())
    return _pretty(result)


@mcp.tool()
async def acc_escalate_dispatch(
    dispatch_id: str,
    reason: str = "",
    severity: str = "warning",
) -> str:
    """Escalate a dispatch to the source claw.

    Args:
        dispatch_id: The dispatch ID.
        reason: Why escalation is needed.
        severity: warning or critical.
    """
    async def _do():
        return await _get_client().escalate(dispatch_id, reason, severity=severity)

    result = await _call(_do())
    return _pretty(result)


@mcp.tool()
async def acc_cancel_dispatch(
    dispatch_id: str,
    reason: str = "",
) -> str:
    """Cancel a dispatch (only the source claw can cancel).

    Args:
        dispatch_id: The dispatch ID.
        reason: Cancellation reason.
    """
    async def _do():
        return await _get_client().cancel(dispatch_id, reason)

    result = await _call(_do())
    return _pretty(result)


# ── Stats & Audit ──


@mcp.tool()
async def acc_get_stats(claw_id: str) -> str:
    """Get dispatch statistics for this claw.

    Args:
        claw_id: Your claw's ID (find it via acc_get_protocol → self.id).
    """
    async def _do():
        return await _get_client().get_stats(claw_id)

    result = await _call(_do())
    return _pretty(result)


@mcp.tool()
async def acc_get_audit_log(
    pipeline_id: str | None = None,
    action: str | None = None,
    page: int = 1,
    page_size: int = 20,
) -> str:
    """Query C2C dispatch audit log.

    Args:
        pipeline_id: Filter by pipeline.
        action: Filter by action (create/accept/reject/complete/...).
        page: Page number.
        page_size: Items per page.
    """
    async def _do():
        return await _get_client().get_audit_log(
            pipeline_id=pipeline_id, action=action,
            page=page, page_size=page_size,
        )

    result = await _call(_do())
    return _pretty(result)


# ── Pipelines ──


@mcp.tool()
async def acc_list_my_pipelines() -> str:
    """List all pipelines this claw belongs to.

    Returns pipeline ID, name, whether this claw is root, and node count.
    Use this to discover which pipelines you participate in, especially
    when you belong to multiple pipelines.
    """
    async def _do():
        return await _get_client().list_my_pipelines()

    result = await _call(_do())
    return _pretty(result)


# ── Resource ──


@mcp.resource("acc://protocol")
async def protocol_resource() -> str:
    """The C2C protocol JSON for this claw (auto-detected pipeline)."""
    try:
        result = await _get_client().get_protocol()
        return _pretty(result)
    except ACCError as e:
        return _pretty({"error": True, "status": e.status, "detail": e.detail})
    except (RuntimeError, httpx.HTTPError) as e:
        return _pretty({"error": True, "detail": str(e)})


# ── Prompt ──


@mcp.prompt()
async def c2c_onboard() -> str:
    """Full C2C workflow guide plus your live pipeline context.

    Returns dispatch lifecycle, connection types, routing rules, and
    your current identity/neighbors in all pipelines you belong to."""

    # -- Fetch pipeline context --
    pipelines_data = None
    single_proto = None
    try:
        client = _get_client()
        pipelines_data = await client.list_my_pipelines()
    except (ACCError, RuntimeError, httpx.HTTPError):
        pass

    pipeline_list = (pipelines_data or {}).get("data", [])

    if len(pipeline_list) == 1:
        try:
            single_proto = await _get_client().get_protocol(pipeline_list[0].get("id"))
        except (ACCError, RuntimeError, httpx.HTTPError):
            pass

    # -- Build context section --
    ctx = ""
    if single_proto:
        si = single_proto.get("self", {})
        pl = single_proto.get("pipeline", {})
        ds = single_proto.get("downstream", [])
        us = single_proto.get("upstream", [])
        role = "ROOT" if si.get("isRoot") else f"CHILD (depth {si.get('depth', '?')})"
        ctx += (
            f"## Your Identity\n\n"
            f"**{si.get('name', '?')}** — {role} in pipeline "
            f"**{pl.get('name', '?')}** ({pl.get('totalNodes', '?')} nodes)\n\n"
            f"Claw ID: `{si.get('id', '?')}`  |  Pipeline ID: `{pl.get('id', '?')}`\n\n"
        )
        if us:
            ctx += "### Upstream\n"
            for u in us:
                ctx += f"  - {u.get('name', '?')} [{u.get('connectionType', '?')}]\n"
            ctx += "\n"
        if ds:
            ctx += "### Downstream\n"
            for d in ds:
                skills = d.get("profile", {}).get("skills", [])
                sk = f" (skills: {', '.join(skills)})" if skills else ""
                ctx += f"  - {d.get('name', '?')} [{d.get('connectionType', '?')}]{sk}\n"
            ctx += "\n"
    elif pipeline_list:
        ctx += "## Your Pipelines\n\n"
        ctx += "You belong to multiple pipelines. Use `routingContext` from each dispatch to determine routing.\n\n"
        for p in pipeline_list:
            root_tag = " **(ROOT)**" if p.get("isRoot") else ""
            ctx += f"  - **{p.get('name', '?')}** (`{p.get('id', '?')}`) — {p.get('nodeCount', '?')} nodes{root_tag}\n"
        ctx += "\nUse `acc_get_protocol(pipeline_id=...)` for details on a specific pipeline.\n\n"
    else:
        ctx += (
            "## No Pipelines Found\n\n"
            "This claw is not part of any pipeline yet. "
            "Ensure ACC_API_KEY is correct and the claw has been added to a pipeline.\n\n"
        )

    # -- Workflow guide (single source of truth) --
    guide = """\
## C2C Workflow Guide

### Dispatch Lifecycle

```
pending ──accept──> accepted ──report──> in_progress ──complete──> completed
   │                                         │
   └──reject──> rejected                     ├──block──> blocked ──unblock──> in_progress
                                             └──escalate──> escalated ──resolve──> in_progress
Any non-terminal ──cancel──> cancelled
```

### Routing: Dispatch-Driven (Stateless)

Each dispatch includes a `routingContext` field — a snapshot of the pipeline topology
at creation time. It tells you:
- Which pipeline this task belongs to
- Your depth and whether you are a leaf node
- Your upstream (who dispatched to you) and downstream (who you can sub-dispatch to)
- Each downstream claw's skills and connection type

**Read `routingContext` from the dispatch. Do NOT cache global pipeline state.**

If `routingContext` is null (legacy dispatch), fall back to `acc_get_protocol(pipeline_id=X)`.

### Role-Specific Behavior

**Root claw** (you initiate work):
1. Call `acc_list_my_pipelines()` to find your pipeline
2. Call `acc_get_protocol(pipeline_id=...)` to see your downstream
3. Use `acc_dispatch()` to assign sub-tasks to downstream claws
4. Monitor with `acc_list_dispatches()` / `acc_get_dispatch()`

**Child claw** (you receive dispatches):
1. Poll `acc_list_dispatches(status="pending")` or receive ServiceBus notification
2. Call `acc_get_dispatch(dispatch_id)` to read full details + `routingContext`
3. `acc_accept_dispatch()` or `acc_reject_dispatch()`
4. Work and `acc_report_progress()` periodically
5. `acc_complete_dispatch()` when done, or `acc_escalate_dispatch()` if stuck
6. If you have downstream claws (in `routingContext`), sub-dispatch as needed

### Connection Types

| Type | Meaning |
|------|---------|
| delegate | Full delegation — target owns the outcome |
| consult | Advisory — target provides input, you own the outcome |
| outsource | External work — target executes, you integrate results |

### Decision Rules

- Multiple downstream claws? Choose based on skills and connection type.
- Task blocked? Report with `report_type="blocker"`, then escalate if unresolved.
- Bidirectional connection? You can both dispatch to and receive from that claw.
- Multiple pipelines? Each dispatch carries its own `routingContext` — no confusion.
"""

    return ctx + guide


def main():
    mcp.run()


if __name__ == "__main__":
    main()
