"""DAG editor API — CRUD operations on World DAG nodes and edges.

Extends the dashboard API with mutating operations for the
live graph editor. All stdlib-only (ADR-028).
"""

from __future__ import annotations

import json

from orxaq.kernel.types import CausalEdge, CausalRole, CycleError, Entity, VariableType


class DAGEditorAPI:
    """CRUD API for the World DAG editor."""

    def __init__(self, engine) -> None:
        self.engine = engine

    @property
    def dag(self):
        return self.engine.world_dag

    def handle(self, path: str, method: str = "GET", body: str = "") -> tuple[dict, int]:
        """Route editor requests."""
        routes: dict[tuple[str, str], object] = {
            ("GET", "/api/dag/nodes"): self._list_nodes,
            ("GET", "/api/dag/edges"): self._list_edges,
            ("GET", "/api/dag/export"): self._export,
            ("POST", "/api/dag/nodes"): self._add_node,
            ("POST", "/api/dag/edges"): self._add_edge,
            ("DELETE", "/api/dag/nodes"): self._remove_node,
            ("DELETE", "/api/dag/edges"): self._remove_edge,
        }

        handler = routes.get((method, path))
        if handler is None:
            return {"error": "not_found", "path": path, "method": method}, 404

        if method in ("POST", "DELETE") and body:
            try:
                data = json.loads(body)
            except json.JSONDecodeError:
                return {"error": "invalid_json"}, 400
            return handler(data)  # type: ignore[no-any-return,operator]

        if method in ("POST", "DELETE"):
            return {"error": "missing_body"}, 400

        return handler()  # type: ignore[no-any-return,operator]

    def _list_nodes(self) -> tuple[dict, int]:
        nodes = []
        for eid, entity in sorted(self.dag._entities.items()):
            nodes.append(
                {
                    "id": eid,
                    "name": entity.name,
                    "description": entity.description,
                    "causal_role": entity.causal_role.value,
                    "variable_type": entity.variable_type.value,
                    "domain": entity.domain,
                }
            )
        return {"nodes": nodes, "count": len(nodes)}, 200

    def _list_edges(self) -> tuple[dict, int]:
        edges = []
        for edge in self.dag._edges.values():
            edges.append(
                {
                    "source": edge.source,
                    "target": edge.target,
                    "mechanism": edge.mechanism,
                    "confidence": edge.confidence,
                    "edge_type": edge.edge_type,
                }
            )
        return {"edges": edges, "count": len(edges)}, 200

    def _export(self) -> tuple[dict, int]:
        nodes = []
        for eid, entity in sorted(self.dag._entities.items()):
            nodes.append(
                {
                    "id": eid,
                    "name": entity.name,
                    "causal_role": entity.causal_role.value,
                    "variable_type": entity.variable_type.value,
                }
            )
        edges = []
        for edge in self.dag._edges.values():
            edges.append(
                {
                    "source": edge.source,
                    "target": edge.target,
                    "mechanism": edge.mechanism,
                    "confidence": edge.confidence,
                }
            )
        return {
            "nodes": nodes,
            "edges": edges,
            "node_count": len(nodes),
            "edge_count": len(edges),
        }, 200

    def _add_node(self, data: dict) -> tuple[dict, int]:
        entity_id = data.get("id", "").strip()
        name = data.get("name", "").strip()
        if not entity_id:
            return {"error": "id is required"}, 400
        if not name:
            name = entity_id

        if self.dag.get_entity(entity_id) is not None:
            return {"error": f"node '{entity_id}' already exists"}, 409

        role_str = data.get("causal_role", "exogenous")
        vtype_str = data.get("variable_type", "static")

        try:
            role = CausalRole(role_str)
        except ValueError:
            role = CausalRole.EXOGENOUS

        try:
            vtype = VariableType(vtype_str)
        except ValueError:
            vtype = VariableType.STATIC

        entity = Entity(
            entity_id=entity_id,
            name=name,
            description=data.get("description", ""),
            causal_role=role,
            variable_type=vtype,
            domain=data.get("domain", ""),
        )
        self.dag.add_entity(entity)

        self.engine.audit_log.append(
            action="dag.add_node",
            actor="editor",
            details=f"Added node: {entity_id}",
        )

        return {"status": "created", "node": entity_id}, 201

    def _add_edge(self, data: dict) -> tuple[dict, int]:
        source = data.get("source", "").strip()
        target = data.get("target", "").strip()
        if not source or not target:
            return {"error": "source and target are required"}, 400

        mechanism = data.get("mechanism", "")
        confidence = float(data.get("confidence", 0.5))

        edge = CausalEdge(
            source=source,
            target=target,
            mechanism=mechanism,
            confidence=confidence,
        )

        try:
            self.dag.add_edge(edge)
        except CycleError as e:
            return {"error": str(e)}, 409
        except ValueError as e:
            return {"error": str(e)}, 400

        self.engine.audit_log.append(
            action="dag.add_edge",
            actor="editor",
            details=f"Added edge: {source} -> {target}",
        )

        return {"status": "created", "edge": f"{source}->{target}"}, 201

    def _remove_node(self, data: dict) -> tuple[dict, int]:
        entity_id = data.get("id", "").strip()
        if not entity_id:
            return {"error": "id is required"}, 400

        if self.dag.get_entity(entity_id) is None:
            return {"error": f"node '{entity_id}' not found"}, 404

        self.dag.remove_entity(entity_id)
        self.engine.audit_log.append(
            action="dag.remove_node",
            actor="editor",
            details=f"Removed node: {entity_id}",
        )
        return {"status": "deleted", "node": entity_id}, 200

    def _remove_edge(self, data: dict) -> tuple[dict, int]:
        source = data.get("source", "").strip()
        target = data.get("target", "").strip()
        if not source or not target:
            return {"error": "source and target are required"}, 400

        edge_id = f"{source}\u2192{target}"
        if edge_id not in self.dag._edges:
            return {"error": f"edge '{source}->{target}' not found"}, 404

        self.dag._children_map.get(source, set()).discard(target)
        self.dag._parents_map.get(target, set()).discard(source)
        del self.dag._edges[edge_id]

        self.engine.audit_log.append(
            action="dag.remove_edge",
            actor="editor",
            details=f"Removed edge: {source} -> {target}",
        )
        return {"status": "deleted", "edge": f"{source}->{target}"}, 200
