"""
Phase 76: Cross-Swarm Dependency Tracking

Swarm A can't finish until Swarm B delivers.  This module tracks inter-swarm
dependencies, detects circular chains, and notifies dependents when blockers
are cleared.

Storage:
  state/broker/cross_deps/deps.json
  state/broker/cross_deps/notifications.jsonl
"""

import json
import uuid
from collections import deque
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

from .server import BROKER_DIR, _write_json_sync, _read_jsonl_sync, _append_jsonl_sync

CROSS_DEPS_DIR = BROKER_DIR / "cross_deps"


def _ensure_dir() -> None:
    CROSS_DEPS_DIR.mkdir(parents=True, exist_ok=True)


def _deps_path() -> Path:
    return CROSS_DEPS_DIR / "deps.json"


def _notifications_path() -> Path:
    return CROSS_DEPS_DIR / "notifications.jsonl"


def _load_deps() -> dict:
    """Load the full dependency store.

    Structure::

        {
            "dependencies": [
                {
                    "id": "<uuid>",
                    "swarm_id": "A",
                    "depends_on": "B",
                    "description": "",
                    "created_at": "...",
                    "met": false
                },
                ...
            ]
        }
    """
    path = _deps_path()
    if not path.exists():
        return {"dependencies": []}
    try:
        return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return {"dependencies": []}


def _save_deps(store: dict) -> None:
    _ensure_dir()
    _write_json_sync(_deps_path(), store)


def _log_notification(event: dict) -> None:
    _ensure_dir()
    _append_jsonl_sync(_notifications_path(), event)


# ── Public API ────────────────────────────────────────────────────────────


def declare_dependency(
    swarm_id: str,
    depends_on_swarm_id: str,
    description: str = "",
) -> dict:
    """Declare that *swarm_id* depends on *depends_on_swarm_id*.

    Returns the created dependency record or an error dict.
    """
    if swarm_id == depends_on_swarm_id:
        return {"ok": False, "error": "A swarm cannot depend on itself"}

    if detect_circular(swarm_id, depends_on_swarm_id):
        return {
            "ok": False,
            "error": f"Adding dependency would create a cycle: "
                     f"{depends_on_swarm_id} transitively depends on {swarm_id}",
        }

    store = _load_deps()

    # Prevent duplicate
    for dep in store["dependencies"]:
        if dep["swarm_id"] == swarm_id and dep["depends_on"] == depends_on_swarm_id:
            return {"ok": True, "note": "Dependency already declared", "dependency": dep}

    record = {
        "id": uuid.uuid4().hex[:12],
        "swarm_id": swarm_id,
        "depends_on": depends_on_swarm_id,
        "description": description,
        "created_at": datetime.now(timezone.utc).isoformat(),
        "met": False,
    }
    store["dependencies"].append(record)
    _save_deps(store)
    return {"ok": True, "dependency": record}


def get_dependencies(swarm_id: str) -> list[dict]:
    """Return what *swarm_id* depends on."""
    store = _load_deps()
    return [d for d in store["dependencies"] if d["swarm_id"] == swarm_id]


def get_dependents(swarm_id: str) -> list[dict]:
    """Return which swarms depend on *swarm_id*."""
    store = _load_deps()
    return [d for d in store["dependencies"] if d["depends_on"] == swarm_id]


def check_dependencies_met(swarm_id: str) -> dict:
    """Check whether all dependencies for *swarm_id* are satisfied.

    Returns ``{"all_met": bool, "blocking": [list of swarm_ids still incomplete]}``.
    """
    deps = get_dependencies(swarm_id)
    blocking = [d["depends_on"] for d in deps if not d["met"]]
    return {"all_met": len(blocking) == 0, "blocking": blocking}


def notify_completion(swarm_id: str) -> dict:
    """Mark *swarm_id* as complete — clears it as a blocker for dependents.

    Returns a summary of affected dependencies and notifications sent.
    """
    store = _load_deps()
    affected = []
    for dep in store["dependencies"]:
        if dep["depends_on"] == swarm_id and not dep["met"]:
            dep["met"] = True
            affected.append(dep)
    _save_deps(store)

    now = datetime.now(timezone.utc).isoformat()
    for dep in affected:
        _log_notification({
            "type": "dependency_cleared",
            "completed_swarm": swarm_id,
            "waiting_swarm": dep["swarm_id"],
            "dependency_id": dep["id"],
            "timestamp": now,
        })

    return {"notified": len(affected), "swarms_unblocked": [d["swarm_id"] for d in affected]}


def get_dependency_graph() -> dict:
    """Return the full graph: nodes (unique swarm IDs) and edges (dependencies)."""
    store = _load_deps()
    nodes: set[str] = set()
    edges: list[dict] = []

    for dep in store["dependencies"]:
        nodes.add(dep["swarm_id"])
        nodes.add(dep["depends_on"])
        edges.append({
            "from": dep["swarm_id"],
            "to": dep["depends_on"],
            "met": dep["met"],
            "description": dep.get("description", ""),
        })

    return {"nodes": sorted(nodes), "edges": edges}


def detect_circular(swarm_id: str, depends_on: str) -> bool:
    """Return True if adding swarm_id -> depends_on would create a cycle.

    Uses BFS from *depends_on* to see if we can reach *swarm_id*.
    """
    if swarm_id == depends_on:
        return True

    store = _load_deps()

    # Build adjacency: X -> [things X depends on]
    adj: dict[str, list[str]] = {}
    for dep in store["dependencies"]:
        adj.setdefault(dep["swarm_id"], []).append(dep["depends_on"])

    # Temporarily add the proposed edge
    adj.setdefault(swarm_id, []).append(depends_on)

    # BFS from depends_on, following *its* dependencies, to see if we reach swarm_id
    visited: set[str] = set()
    queue = deque([depends_on])
    while queue:
        current = queue.popleft()
        if current in visited:
            continue
        visited.add(current)
        for neighbour in adj.get(current, []):
            if neighbour == swarm_id:
                return True
            if neighbour not in visited:
                queue.append(neighbour)

    return False


def remove_dependency(swarm_id: str, depends_on_swarm_id: str) -> dict:
    """Remove a declared dependency."""
    store = _load_deps()
    original_len = len(store["dependencies"])
    store["dependencies"] = [
        d for d in store["dependencies"]
        if not (d["swarm_id"] == swarm_id and d["depends_on"] == depends_on_swarm_id)
    ]
    if len(store["dependencies"]) == original_len:
        return {"ok": False, "error": "Dependency not found"}
    _save_deps(store)
    return {"ok": True}


def get_notifications(limit: int = 50) -> list[dict]:
    """Return recent notifications from the notifications log."""
    entries = _read_jsonl_sync(_notifications_path())
    return entries[-limit:]
