"""Spoke (distributed agent node) endpoints."""

from __future__ import annotations

import logging
import os
from datetime import UTC

from aiohttp import web

from dobby.models import Spoke

logger = logging.getLogger(__name__)


async def register_spoke(request: web.Request) -> web.Response:
    """Register or update a spoke agent node."""
    store = request.app["store"]
    body = await request.json()
    spoke = Spoke(
        id=body["id"],
        capabilities=body.get("capabilities", ["claude", "codex"]),
        hostname=body.get("hostname", ""),
        system_info=body.get("system_info", {}),
    )
    await store.register_spoke(spoke)
    return web.json_response(spoke.to_dict(), status=201)


async def spoke_heartbeat(request: web.Request) -> web.Response:
    """Update spoke heartbeat and system info."""
    store = request.app["store"]
    spoke_id = request.match_info["id"]
    body = await request.json()
    from datetime import datetime
    await store.update_spoke(spoke_id,
        status=body.get("status", "online"),
        last_heartbeat=datetime.now(UTC).isoformat(),
        running_tasks=body.get("running_tasks", 0),
        system_info=body.get("system_info", {}),
    )
    return web.json_response({"ok": True})


async def list_spokes(request: web.Request) -> web.Response:
    """List all registered spokes."""
    store = request.app["store"]
    spokes = await store.list_spokes()
    # Mark spokes as offline if no heartbeat in 90s
    from datetime import datetime, timedelta
    now = datetime.now(UTC)
    result = []
    for s in spokes:
        d = s.to_dict()
        if (now - s.last_heartbeat) > timedelta(seconds=90):
            d["status"] = "offline"
        result.append(d)
    return web.json_response(result)


async def spoke_claim_task(request: web.Request) -> web.Response:
    """Spoke claims the next available task targeted at it."""
    store = request.app["store"]
    spoke_id = request.match_info["id"]
    task = await store.claim_task_for_spoke(spoke_id)
    if task is None:
        return web.json_response({"task": None})

    # Create execution span for trajectory tracking
    if task.trace_id:
        try:
            from dobby.core.trajectory import trajectory_parser
            results_dir = request.app.get("results_dir", "results")
            log_path = os.path.join(results_dir, task.id, "agent.log")
            await trajectory_parser.add_span(
                store, task.trace_id, kind="execution",
                task_id=task.id, log_path=log_path,
                metadata={"spoke_id": spoke_id},
            )
        except Exception:
            logger.debug("Trace execution span failed", exc_info=True)

    # Also return the project info so spoke knows the repo path
    project = await store.get_project(task.project)
    return web.json_response({
        "task": task.to_dict(),
        "project": project.to_dict() if project else None,
    })


async def spoke_submit_result(request: web.Request) -> web.Response:
    """Spoke submits a task result and fires notifications."""
    store = request.app["store"]
    body = await request.json()
    try:
        from dobby.models import Result
        result = Result.from_dict(body)

        # Fix garbled summaries from spokes running old collector code:
        # If summary looks like raw JSON init line, try to extract meaningful text
        if result.summary and result.summary.startswith('{"type":"system"'):
            # The spoke sent raw stream-json init as summary — replace with a useful fallback
            result.summary = f"Task {result.status} (summary not parsed — check logs for details)"

        await store.store_result(result)
        await store.update_task(result.task_id, status=result.status)

        # Fire Slack/SSE notifications (same as daemon dispatch loop)
        event_bus = request.app.get("event_bus")
        if event_bus:
            await event_bus.publish("task_status", {
                "task_id": result.task_id,
                "status": result.status,
                "summary": result.summary,
            })

        slack_notifier = request.app.get("slack_notifier")
        task = await store.get_task(result.task_id)
        task_dict = task.to_dict()
        result_dict = result.to_dict()

        # Add result span to trace
        trace_id = task.trace_id
        if trace_id:
            try:
                from dobby.core.trajectory import trajectory_parser
                cost_info = result.cost or {}
                await trajectory_parser.add_span(
                    store, trace_id, kind="result", task_id=result.task_id,
                    metadata={
                        "status": result.status,
                        "summary": result.summary[:200] if result.summary else None,
                        "cost_usd": cost_info.get("cost_usd", 0.0),
                    },
                )
            except Exception:
                logger.debug("Trace result span failed", exc_info=True)

        if slack_notifier:
            try:
                if result.status == "completed":
                    await slack_notifier.notify_completed(task_dict, result_dict)
                else:
                    await slack_notifier.notify_failed(task_dict, result_dict)
            except Exception as exc:
                logger.warning("Spoke result notification failed: %s", exc)

        # Notification span + finalize trace
        if trace_id:
            try:
                from dobby.core.trajectory import trajectory_parser
                await trajectory_parser.add_span(
                    store, trace_id, kind="notification",
                    metadata={"notified": bool(slack_notifier)},
                )
                cost_info = result.cost or {}
                await trajectory_parser.finalize_trace(
                    store, trace_id,
                    status=result.status,
                    total_input_tokens=cost_info.get("input_tokens", 0),
                    total_output_tokens=cost_info.get("output_tokens", 0),
                    total_cost_usd=cost_info.get("cost_usd", 0.0),
                )
            except Exception:
                logger.debug("Trace finalization failed", exc_info=True)

        return web.json_response({"ok": True}, status=201)
    except Exception as exc:
        logger.exception("spoke_submit_result failed: %s", exc)
        return web.json_response({"error": str(exc)}, status=500)


async def spoke_append_log(request: web.Request) -> web.Response:
    """Spoke appends log lines for a running task."""
    task_id = request.match_info["task_id"]
    body = await request.json()
    lines = body.get("lines", "")
    if not lines:
        return web.json_response({"ok": True})

    results_dir = request.app.get("results_dir", "results")
    task_dir = os.path.join(results_dir, task_id)
    os.makedirs(task_dir, exist_ok=True)
    log_path = os.path.join(task_dir, "agent.log")
    with open(log_path, "a", encoding="utf-8") as f:
        f.write(lines)

    # Parse each line for trajectory tracking (lightweight per-line processing)
    store = request.app["store"]
    try:
        from dobby.core.trajectory import trajectory_parser
        for line in lines.splitlines():
            if line.strip():
                await trajectory_parser.ingest_log_line(store, task_id, line)
        await store.commit()
    except Exception:
        logger.debug("Trajectory ingest failed for %s", task_id, exc_info=True)

    return web.json_response({"ok": True})
