"""
Phase 15: Cross-Swarm Task Dependencies

DAG-based task dependency manager. Tasks cannot start until all their
prerequisites are complete. Includes cycle detection, topological sorting,
and transitive readiness checks.
"""

import json
from collections import deque
from pathlib import Path
from typing import Optional


class DependencyManager:
    def __init__(self, broker_dir: str = "state/broker"):
        self.broker_dir = Path(broker_dir)
        self.tasks_dir = self.broker_dir / "tasks"
        self.tasks_dir.mkdir(parents=True, exist_ok=True)

    def _load_task(self, task_id: str) -> Optional[dict]:
        path = self.tasks_dir / f"{task_id}.json"
        if not path.exists():
            return None
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)

    def _save_task(self, task_id: str, data: dict) -> None:
        with open(self.tasks_dir / f"{task_id}.json", "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2)

    # ── Cycle Detection ───────────────────────────────────────────────────

    def _would_create_cycle(self, target: str, dependency: str) -> bool:
        """Check if adding target->dependency would create a cycle.
        Uses BFS from dependency to see if we can reach target."""
        visited = set()
        queue = deque([dependency])

        while queue:
            current = queue.popleft()
            if current == target:
                return True
            if current in visited:
                continue
            visited.add(current)

            task = self._load_task(current)
            if not task:
                continue
            for dep_id in task.get("dependencies", []):
                if dep_id not in visited:
                    queue.append(dep_id)

        return False

    # ── Core Operations ───────────────────────────────────────────────────

    def add_dependency(self, target_task_id: str, depends_on_task_id: str) -> dict:
        """Add a dependency: target cannot start until depends_on is complete.

        Returns {"ok": True} or {"ok": False, "error": "reason"}.
        """
        if target_task_id == depends_on_task_id:
            return {"ok": False, "error": "A task cannot depend on itself"}

        target = self._load_task(target_task_id)
        if not target:
            return {"ok": False, "error": f"Task '{target_task_id}' not found"}

        dep = self._load_task(depends_on_task_id)
        if not dep:
            return {"ok": False, "error": f"Dependency task '{depends_on_task_id}' not found"}

        # Cycle check
        if self._would_create_cycle(target_task_id, depends_on_task_id):
            return {
                "ok": False,
                "error": f"Adding this dependency would create a cycle: "
                         f"{depends_on_task_id} transitively depends on {target_task_id}",
            }

        deps = target.get("dependencies", [])
        if depends_on_task_id in deps:
            return {"ok": True, "note": "Dependency already exists"}

        deps.append(depends_on_task_id)
        target["dependencies"] = deps
        self._save_task(target_task_id, target)
        return {"ok": True}

    def remove_dependency(self, target_task_id: str, depends_on_task_id: str) -> dict:
        """Remove a dependency link."""
        target = self._load_task(target_task_id)
        if not target:
            return {"ok": False, "error": f"Task '{target_task_id}' not found"}

        deps = target.get("dependencies", [])
        if depends_on_task_id not in deps:
            return {"ok": False, "error": "Dependency not found"}

        deps.remove(depends_on_task_id)
        target["dependencies"] = deps
        self._save_task(target_task_id, target)
        return {"ok": True}

    def is_task_ready(self, task_id: str) -> dict:
        """Check if a task's dependencies are all satisfied.

        Returns {"ready": bool, "blocking": [...list of unfinished dep IDs]}.
        """
        task = self._load_task(task_id)
        if not task:
            return {"ready": False, "blocking": [], "error": f"Task '{task_id}' not found"}

        blocking = []
        for dep_id in task.get("dependencies", []):
            dep = self._load_task(dep_id)
            if not dep or dep.get("status") != "completed":
                blocking.append(dep_id)

        return {"ready": len(blocking) == 0, "blocking": blocking}

    def get_dependencies(self, task_id: str) -> dict:
        """Get direct and transitive dependencies for a task."""
        task = self._load_task(task_id)
        if not task:
            return {"direct": [], "transitive": [], "error": f"Task '{task_id}' not found"}

        direct = task.get("dependencies", [])

        # BFS for transitive deps
        transitive = []
        visited = set()
        queue = deque(direct)
        while queue:
            current = queue.popleft()
            if current in visited:
                continue
            visited.add(current)
            transitive.append(current)
            dep_task = self._load_task(current)
            if dep_task:
                for sub_dep in dep_task.get("dependencies", []):
                    if sub_dep not in visited:
                        queue.append(sub_dep)

        return {"direct": direct, "transitive": transitive}

    def get_dependents(self, task_id: str) -> list[str]:
        """Find all tasks that depend on the given task (reverse lookup)."""
        dependents = []
        for f in self.tasks_dir.glob("*.json"):
            try:
                data = json.loads(f.read_text("utf-8"))
                if task_id in data.get("dependencies", []):
                    dependents.append(data.get("task_id", f.stem))
            except (json.JSONDecodeError, OSError):
                continue
        return dependents

    # ── Topological Sort ──────────────────────────────────────────────────

    def execution_order(self, task_ids: list[str]) -> dict:
        """Return a valid execution order for the given tasks using topological sort.

        Returns {"order": [...], "has_cycle": False} or {"order": [], "has_cycle": True}.
        """
        # Build adjacency: task -> set of deps that are in our task_ids set
        task_set = set(task_ids)
        in_degree: dict[str, int] = {tid: 0 for tid in task_ids}
        dependents: dict[str, list[str]] = {tid: [] for tid in task_ids}

        for tid in task_ids:
            task = self._load_task(tid)
            if not task:
                continue
            for dep_id in task.get("dependencies", []):
                if dep_id in task_set:
                    in_degree[tid] = in_degree.get(tid, 0) + 1
                    dependents.setdefault(dep_id, []).append(tid)

        # Kahn's algorithm
        queue = deque([tid for tid, deg in in_degree.items() if deg == 0])
        order = []

        while queue:
            current = queue.popleft()
            order.append(current)
            for dependent in dependents.get(current, []):
                in_degree[dependent] -= 1
                if in_degree[dependent] == 0:
                    queue.append(dependent)

        has_cycle = len(order) != len(task_ids)
        return {"order": order, "has_cycle": has_cycle}

    def get_ready_tasks(self, task_ids: Optional[list[str]] = None) -> list[str]:
        """From a set of tasks (or all pending tasks), return those whose
        dependencies are all completed — i.e., ready to execute now."""
        if task_ids is None:
            task_ids = []
            for f in self.tasks_dir.glob("*.json"):
                try:
                    data = json.loads(f.read_text("utf-8"))
                    if data.get("status") in ("pending", "open"):
                        task_ids.append(data.get("task_id", f.stem))
                except (json.JSONDecodeError, OSError):
                    continue

        ready = []
        for tid in task_ids:
            result = self.is_task_ready(tid)
            if result.get("ready"):
                ready.append(tid)
        return ready
