"""Dobby MCP server — exposes Dobby tools to Claude Code."""
from __future__ import annotations

import json
import os

from mcp.server.fastmcp import FastMCP

from dobby.cli.client import DobbyClient

server = FastMCP(
    name="dobby",
    instructions=(
        "Dobby is a meta-agent orchestrator. Use these tools to queue tasks, "
        "dispatch Claude Code agents across projects, monitor GPU jobs, and "
        "track results. The Dobby daemon must be running for these tools to work."
    ),
)

_client: DobbyClient | None = None


def get_client() -> DobbyClient:
    global _client
    if _client is None:
        url = os.environ.get("DOBBY_URL", "http://127.0.0.1:8420")
        _client = DobbyClient(base_url=url)
    return _client


@server.tool()
async def dobby_add_task(
    project: str,
    description: str,
    exit_criteria: str | None = None,
    priority: str | None = None,
    cron: str | None = None,
    every: str | None = None,
    after: str | None = None,
    infra: str | None = None,
    checkpoint: bool = False,
    session_id: str | None = None,
    backend: str | None = None,
    target: str | None = None,
    timeout: int | None = None,
) -> str:
    """Add a new task to the Dobby queue. The task will be dispatched to an agent.

    Args:
        project: Name of the registered project (e.g., "spare", "memagent")
        description: What the agent should do
        exit_criteria: When the task is done (e.g., "PR ready", "tests pass", "report results")
        priority: Task priority — "low", "normal" (default), "high", or "urgent"
        cron: Cron schedule expression (e.g., "0 3 * * *" for nightly at 3am)
        every: Interval schedule (e.g., "30m", "2h")
        after: Task ID to run after (dependency chain)
        infra: Name of GPU infra to use from project profile
        checkpoint: If True, agent will ask for approval before big decisions
        session_id: Resume an existing Claude Code session by ID instead of starting fresh
        backend: Agent backend — "claude" (default) or "codex" (OpenAI Codex)
        target: Spoke to run on — "local" (default), spoke ID, or "any" for auto-dispatch
        timeout: Optional timeout in seconds for the task run
    """
    body: dict = {"project": project, "description": description}
    if exit_criteria:
        body["exit_criteria"] = exit_criteria
    if priority:
        body["priority"] = priority
    if checkpoint:
        body["checkpoint"] = True
    if cron:
        body["schedule"] = {"type": "cron", "expr": cron}
    elif every:
        body["schedule"] = {"type": "interval", "every": every}
    elif after:
        body["schedule"] = {"type": "after", "depends_on": after}
    if infra:
        body["infra"] = infra
    if session_id:
        body["session_id"] = session_id
    if backend:
        body["backend"] = backend
    if target:
        body["target"] = target
    if timeout is not None:
        body["timeout"] = timeout

    # Attach trace context for trajectory tracking
    body["_trace_context"] = {
        "source": "mcp",
        "input_text": description[:500],
    }

    result = await get_client().post("/tasks", json=body)
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_batch_add(
    tasks: list[dict],
) -> str:
    """Add a batch of tasks to the Dobby queue. All tasks get the same batch_id for tracking.

    Args:
        tasks: List of task dicts, each with at least 'project' and 'description'.
               Optional fields: exit_criteria, priority, cron, every, after, infra,
               checkpoint, session_id, backend, timeout.

    Example:
        [
            {"project": "spare", "description": "Fix warmup scheduler", "priority": "high"},
            {"project": "spare", "description": "Add eval harness"}
        ]
    """
    result = await get_client().post("/batches", json={"tasks": tasks})
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_plan_task(
    project: str,
    description: str,
    exit_criteria: str | None = None,
    priority: str | None = None,
) -> str:
    """Create a planned task that needs review before execution.
    Use this for complex tasks that need discussion.

    Args:
        project: Name of the registered project
        description: What needs to be planned
        exit_criteria: What "done" looks like
        priority: Task priority — "low", "normal", "high", or "urgent"
    """
    body: dict = {
        "project": project,
        "description": description,
        "mode": "planned",
    }
    if exit_criteria:
        body["exit_criteria"] = exit_criteria
    if priority:
        body["priority"] = priority

    # Attach trace context for trajectory tracking
    body["_trace_context"] = {
        "source": "mcp",
        "input_text": description[:500],
    }

    result = await get_client().post("/tasks", json=body)
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_status(
    project: str | None = None,
    status: str | None = None,
) -> str:
    """Show current task queue — what's running, pending, completed, or failed.

    Args:
        project: Filter by project name (optional)
        status: Filter by status — "pending", "running", "completed", "failed", "cancelled"
            (optional)
    """
    params = {}
    if project:
        params["project"] = project
    if status:
        params["status"] = status

    tasks = await get_client().get("/tasks", params=params if params else None)
    if not tasks:
        return "No tasks found."
    return json.dumps(tasks, indent=2)


@server.tool()
async def dobby_get_task(task_id: str) -> str:
    """Get details of a specific task.

    Args:
        task_id: The task ID (e.g., "t-20260321-a3f2")
    """
    result = await get_client().get(f"/tasks/{task_id}")
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_results(task_id: str) -> str:
    """Get the result of a completed or failed task — summary, artifacts, metrics, cost.

    Args:
        task_id: The task ID to get results for
    """
    result = await get_client().get(f"/results/{task_id}")
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_list_results(
    project: str | None = None,
    last_n: int | None = None,
) -> str:
    """List recent task results, optionally filtered by project.

    Args:
        project: Filter by project name (optional)
        last_n: Number of recent results to return (optional, default all)
    """
    params = {}
    if project:
        params["project"] = project
    if last_n:
        params["last_n"] = str(last_n)

    results = await get_client().get("/results", params=params if params else None)
    if not results:
        return "No results found."
    return json.dumps(results, indent=2)


@server.tool()
async def dobby_cancel(task_id: str) -> str:
    """Cancel a running or pending task.

    Args:
        task_id: The task ID to cancel
    """
    result = await get_client().patch(f"/tasks/{task_id}", json={"status": "cancelled"})
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_retry(task_id: str) -> str:
    """Retry a failed or cancelled task by re-queuing it.

    Args:
        task_id: The task ID to retry
    """
    result = await get_client().post(f"/tasks/{task_id}/retry")
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_detail_status(task_id: str) -> str:
    """Get detailed live status of a running task — current activity, files touched, tools used,
    tokens consumed.

    Args:
        task_id: The task ID to inspect (e.g., "t-20260321-a3f2")
    """
    result = await get_client().get(f"/tasks/{task_id}/detail-status")
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_stats() -> str:
    """Get queue statistics — pending/running/completed counts, token usage, and cost breakdown
    by project."""
    client = get_client()
    result = await client.get("/stats")
    try:
        cost_data = await client.get("/stats/cost")
        result["cost_breakdown"] = cost_data
    except Exception:
        pass
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_projects() -> str:
    """List all registered projects."""
    projects = await get_client().get("/projects")
    if not projects:
        return "No projects registered. Use dobby_register to add one."
    return json.dumps(projects, indent=2)


@server.tool()
async def dobby_register(
    name: str,
    repo: str,
    context: str | None = None,
    git_remote: str | None = None,
) -> str:
    """Register a project with Dobby so tasks can be dispatched to it.

    Args:
        name: Short name for the project (e.g., "spare", "memagent")
        repo: Absolute path to the project directory
        context: Brief description of what this project is
        git_remote: Git remote URL (for remote deployment later)
    """
    body: dict = {"name": name, "repo": repo}
    if context:
        body["context"] = context
    if git_remote:
        body["git_remote"] = git_remote

    result = await get_client().post("/projects", json=body)
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_remember(
    key: str,
    value: str,
    type: str = "general",
) -> str:
    """Save a memory to the Dobby hub. Memories persist in the hub database, survive restarts,
    and are accessible from any session.

    Use this to store information that should be remembered across conversations and agent runs:
    project context, user preferences, feedback, references to external resources, etc.

    Args:
        key: Descriptive key for the memory
            (e.g., "vps-kill-safety", "startup-commands", "distributed-architecture-plan")
        value: The content to remember — can be any text
        type: Memory category — "general" (default), "feedback" (user corrections/preferences),
              "project" (ongoing work context), "user" (user info/preferences),
              or "reference" (external resources)
    """
    result = await get_client().post("/memories", json={"key": key, "value": value, "type": type})
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_recall(
    key: str | None = None,
    query: str | None = None,
    type: str | None = None,
) -> str:
    """Retrieve memories from the Dobby hub. Memories persist across restarts and sessions.

    Three retrieval modes:
    - By exact key: pass key="some-key" for a specific memory
    - By search: pass query="search term" to find memories matching key or value
    - By type: pass type="feedback" to list all memories of that type
    - Combine query + type for filtered search

    Args:
        key: Exact key to look up (returns single memory or "not found")
        query: Search term to match against key and value (substring match)
        type: Filter by type — "general", "feedback", "project", "user", or "reference"
    """
    if key:
        result = await get_client().get(f"/memories/{key}")
        return json.dumps(result, indent=2)
    params = {}
    if query:
        params["query"] = query
    if type:
        params["type"] = type
    result = await get_client().get("/memories", params=params if params else None)
    if not result:
        return "No memories found."
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_forget(key: str) -> str:
    """Delete a memory from the Dobby hub.

    Args:
        key: The key of the memory to delete
    """
    result = await get_client().delete(f"/memories/{key}")
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_user_prefs(
    action: str = "get",
    key: str | None = None,
    value: str | None = None,
    query: str | None = None,
) -> str:
    """Get or set user preferences that Dobby agents use when executing tasks.

    Preferences tell agents HOW to behave — e.g., "always reply to advisor emails myself",
    "prefer morning meetings", "exercise reminder at 6pm". Agents query preferences before
    taking actions so they respect user wishes.

    Actions:
    - "get": Retrieve a specific preference by key
    - "set": Save or update a preference (requires key and value)
    - "list": List all preferences
    - "search": Search preferences by keyword (requires query)
    - "delete": Remove a preference (requires key)

    Args:
        action: One of "get", "set", "list", "search", "delete"
        key: Preference key (e.g., "email-reply-policy", "exercise-time", "meeting-preferences")
        value: Preference value (for "set" action)
        query: Search term (for "search" action)
    """
    client = get_client()

    if action == "set":
        if not key or not value:
            return "Error: 'key' and 'value' are required for set action"
        result = await client.post(
            "/memories",
            json={"key": f"pref:{key}", "value": value, "type": "preference"},
        )
        return json.dumps(result, indent=2)

    elif action == "get":
        if not key:
            return "Error: 'key' is required for get action"
        result = await client.get(f"/memories/pref:{key}")
        return json.dumps(result, indent=2)

    elif action == "list":
        result = await client.get("/memories", params={"type": "preference"})
        if not result:
            return "No user preferences set."
        return json.dumps(result, indent=2)

    elif action == "search":
        if not query:
            return "Error: 'query' is required for search action"
        result = await client.get(
            "/memories", params={"query": query, "type": "preference"}
        )
        if not result:
            return f"No preferences matching \"{query}\"."
        return json.dumps(result, indent=2)

    elif action == "delete":
        if not key:
            return "Error: 'key' is required for delete action"
        result = await client.delete(f"/memories/pref:{key}")
        return json.dumps(result, indent=2)

    else:
        return f"Error: Unknown action '{action}'. Use get, set, list, search, or delete."


@server.tool()
async def dobby_resume(
    task_id: str,
    description: str,
    exit_criteria: str | None = None,
    priority: str | None = None,
    target: str | None = None,
) -> str:
    """Resume a completed/failed task's Claude Code session with a new follow-up task.

    Looks up the session_id from the original task's result, then creates a new task
    in the same project/target that resumes that session instead of starting fresh.

    Args:
        task_id: The original task ID whose session to resume (e.g., "t-20260321-a3f2")
        description: What the resumed agent should do next
        exit_criteria: When the follow-up task is done
        priority: Task priority — "low", "normal" (default), "high", or "urgent"
        target: Override spoke target (defaults to original task's target)
    """
    client = get_client()

    # Get session_id from the task's result
    session_data = await client.get(f"/tasks/{task_id}/session")
    session_id = session_data.get("session_id")
    if not session_id:
        return json.dumps({"error": f"No session_id found for task {task_id}"})

    # Get original task for project/target
    task = await client.get(f"/tasks/{task_id}")

    body: dict = {
        "project": task["project"],
        "description": description,
        "session_id": session_id,
    }
    if target:
        body["target"] = target
    elif task.get("target"):
        body["target"] = task["target"]
    if exit_criteria:
        body["exit_criteria"] = exit_criteria
    if priority:
        body["priority"] = priority

    result = await client.post("/tasks", json=body)
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_add_todo(
    description: str,
    project: str | None = None,
    due_date: str | None = None,
    priority: str | None = None,
    tags: list[str] | None = None,
) -> str:
    """Add a todo/milestone item to track. Todos are NOT dispatched tasks — they're tracking items
    like "Review PR #4", "COLM deadline April 15", "Check eval results".

    Args:
        description: What needs to be done
        project: Project name (optional — can be cross-project)
        due_date: Due date in ISO format (e.g., "2026-04-15", "2026-03-25T09:00:00")
        priority: Priority — "low", "normal" (default), "high", or "urgent"
        tags: Tags for categorization (e.g., ["pr", "review"], ["deadline"])
    """
    body: dict = {"description": description}
    if project:
        body["project"] = project
    if due_date:
        body["due_date"] = due_date
    if priority:
        body["priority"] = priority
    if tags:
        body["tags"] = tags
    result = await get_client().post("/todos", json=body)
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_list_todos(
    project: str | None = None,
    status: str | None = None,
    due_date_before: str | None = None,
) -> str:
    """List todos/milestones, optionally filtered.

    Args:
        project: Filter by project name (optional)
        status: Filter by status — "open" (default), "done", "cancelled"
        due_date_before: Show todos due before this date (ISO format)
    """
    params = {}
    if project:
        params["project"] = project
    if status:
        params["status"] = status
    if due_date_before:
        params["due_date_before"] = due_date_before
    result = await get_client().get("/todos", params=params if params else None)
    if not result:
        return "No todos found."
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_update_todo(
    id: str,
    status: str | None = None,
    priority: str | None = None,
    due_date: str | None = None,
    description: str | None = None,
) -> str:
    """Update a todo — mark done, change priority, update due date, etc.

    Args:
        id: The todo ID (e.g., "todo-20260324-a1b2c3d4")
        status: New status — "open", "done", or "cancelled"
        priority: New priority — "low", "normal", "high", "urgent"
        due_date: New due date (ISO format)
        description: Updated description
    """
    body: dict = {}
    if status:
        body["status"] = status
    if priority:
        body["priority"] = priority
    if due_date:
        body["due_date"] = due_date
    if description:
        body["description"] = description
    if not body:
        return "Error: No fields to update. Provide status, priority, due_date, or description."
    result = await get_client().patch(f"/todos/{id}", json=body)
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_delete_todo(id: str) -> str:
    """Delete a todo item.

    Args:
        id: The todo ID to delete
    """
    result = await get_client().delete(f"/todos/{id}")
    return json.dumps(result, indent=2)


@server.tool()
async def dobby_plan_day(
    timezone: str = "America/Los_Angeles",
    date: str | None = None,
    calendar_events: str | None = None,
    project_weights: str | None = None,
) -> str:
    """Generate a daily plan with time blocks for your projects.

    Call this after reading your Google Calendar events for the day.
    Pass the calendar events as a JSON string and Dobby will create
    optimized focus blocks, breaks, and lunch around your meetings.

    Workflow:
    1. Use google-calendar MCP to read today's events
    2. Call this tool with those events
    3. Use google-calendar MCP to create the returned time blocks as events

    Args:
        timezone: Your IANA timezone (e.g., "America/Los_Angeles")
        date: Date to plan (YYYY-MM-DD, defaults to today)
        calendar_events: JSON string of calendar events from Google Calendar
        project_weights: JSON string of project weights, e.g.,
            [{"name": "spare", "weight": 3}, {"name": "adpo", "weight": 2}]
            If omitted, uses all registered projects with equal weight.
    """
    client = get_client()

    import json as _json
    from datetime import datetime as _dt
    from zoneinfo import ZoneInfo as _ZI

    tz = _ZI(timezone)
    plan_date = date or _dt.now(tz).strftime("%Y-%m-%d")

    events = _json.loads(calendar_events) if calendar_events else []

    if project_weights:
        projects = _json.loads(project_weights)
    else:
        all_projects = await client.get("/projects")
        projects = [{"name": p["name"], "weight": 1, "priority": "normal"} for p in all_projects]

    # Fetch open todos
    todos_raw = await client.get("/todos", params={"status": "open"})
    todos = todos_raw if todos_raw else []

    # Generate the plan via the API
    body = {
        "date": plan_date,
        "timezone": timezone,
        "calendar_events": events,
        "projects": projects,
        "todos": todos,
    }
    plan = await client.post("/planner/generate", json=body)

    # Format response
    blocks = plan.get("blocks", [])
    lines = [f"Daily Plan for {plan_date}\n"]
    for b in blocks:
        emoji = {
            "focus": "[FOCUS]", "meeting": "[MEETING]", "break": "[BREAK]",
            "lunch": "[LUNCH]", "buffer": "[BUFFER]",
        }.get(b["block_type"], "[OTHER]")
        start_str = b["start"].split("T")[1][:5] if "T" in b["start"] else b["start"]
        end_str = b["end"].split("T")[1][:5] if "T" in b["end"] else b["end"]
        proj = f" [{b['project']}]" if b.get("project") else ""
        lines.append(
            f"{emoji} {start_str}-{end_str} {b['title']}{proj} ({b['duration_minutes']}min)"
        )

    focus_mins = sum(b["duration_minutes"] for b in blocks if b["block_type"] == "focus")
    lines.append(f"\nTotal blocks: {len(blocks)}")
    lines.append(f"Focus time: {focus_mins // 60}h {focus_mins % 60}m")

    # Instructions for the agent to create calendar events
    lines.append("\n--- CREATE THESE AS CALENDAR EVENTS ---")
    lines.append(
        "Use google-calendar MCP tool (gcal_create_event) to create each focus/break/lunch block."
    )
    lines.append("Use sendUpdates='none' to avoid notification spam.")
    for b in blocks:
        if b.get("source") == "planner":
            color = b.get("color_id", "9")
            lines.append(
                f"  gcal_create_event: summary=\"{b['title']}\", "
                f"start={{dateTime: \"{b['start']}\"}}, "
                f"end={{dateTime: \"{b['end']}\"}}, "
                f"colorId=\"{color}\""
            )

    return "\n".join(lines)


def main():
    """Entry point for dobby-mcp command."""
    server.run(transport="stdio")


if __name__ == "__main__":
    main()
