from dataclasses import dataclass, field
from typing import Any, Literal


TaskKind = Literal["tool_call"]


@dataclass(slots=True)
class TaskNode:
    id: str
    kind: TaskKind
    tool_name: str
    args: dict[str, Any] = field(default_factory=dict)
    depends_on: list[str] = field(default_factory=list)
    side_effect: bool = False

    def to_dict(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "kind": self.kind,
            "tool_name": self.tool_name,
            "args": dict(self.args),
            "depends_on": list(self.depends_on),
            "side_effect": self.side_effect,
        }


@dataclass(slots=True)
class TaskGraph:
    goal: str
    protocol: str
    domain: str
    display_goal: str = ""
    tasks: list[TaskNode] = field(default_factory=list)
    metadata: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return {
            "goal": self.goal,
            "protocol": self.protocol,
            "domain": self.domain,
            "display_goal": self.display_goal,
            "tasks": [task.to_dict() for task in self.tasks],
            "metadata": dict(self.metadata),
        }


@dataclass(slots=True)
class TaskGraphValidationResult:
    valid: bool
    errors: list[str] = field(default_factory=list)
    blocked_tools: list[str] = field(default_factory=list)
    disabled_tools: list[str] = field(default_factory=list)
    approval_required_tools: list[str] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        return {
            "valid": self.valid,
            "errors": list(self.errors),
            "blocked_tools": list(self.blocked_tools),
            "disabled_tools": list(self.disabled_tools),
            "approval_required_tools": list(self.approval_required_tools),
        }


def task_graph_from_direct_calls(
    goal: str,
    *,
    protocol: str,
    domain: str,
    calls: list[tuple[str, dict[str, Any]]],
    display_goal: str = "",
    metadata: dict[str, Any] | None = None,
    side_effect: bool = False,
    dependency_mode: Literal["chain", "independent"] = "chain",
) -> TaskGraph:
    tasks: list[TaskNode] = []
    prior_id = ""
    for index, (tool_name, args) in enumerate(calls, start=1):
        task_id = f"t{index}"
        depends_on = [prior_id] if (dependency_mode == "chain" and prior_id) else []
        tasks.append(TaskNode(
            id=task_id,
            kind="tool_call",
            tool_name=str(tool_name),
            args=dict(args or {}),
            depends_on=depends_on,
            side_effect=side_effect,
        ))
        prior_id = task_id
    return TaskGraph(
        goal=goal,
        protocol=protocol,
        domain=domain,
        display_goal=display_goal,
        tasks=tasks,
        metadata=dict(metadata or {}),
    )


def _has_cycle(tasks: list[TaskNode]) -> bool:
    task_map = {task.id: task for task in tasks}
    visiting: set[str] = set()
    visited: set[str] = set()

    def visit(task_id: str) -> bool:
        if task_id in visited:
            return False
        if task_id in visiting:
            return True
        visiting.add(task_id)
        task = task_map.get(task_id)
        if task is not None:
            for dep in task.depends_on:
                if dep in task_map and visit(dep):
                    return True
        visiting.remove(task_id)
        visited.add(task_id)
        return False

    return any(visit(task.id) for task in tasks)


def validate_task_graph(
    graph: TaskGraph,
    *,
    blocked_tools: set[str] | None = None,
    disabled_tools: set[str] | None = None,
    approval_tools: set[str] | None = None,
    max_tasks: int = 20,
    max_depth: int = 8,
    allow_side_effects: bool = False,
) -> TaskGraphValidationResult:
    blocked = blocked_tools or set()
    disabled = disabled_tools or set()
    approval = approval_tools or set()
    errors: list[str] = []
    blocked_hits: list[str] = []
    disabled_hits: list[str] = []
    approval_hits: list[str] = []

    if len(graph.tasks) > max_tasks:
        errors.append(f"Task graph exceeds max_tasks ({len(graph.tasks)} > {max_tasks})")

    if _has_cycle(graph.tasks):
        errors.append("Task graph contains a dependency cycle")

    task_ids = {task.id for task in graph.tasks}
    for task in graph.tasks:
        if len(task.depends_on) > max_depth:
            errors.append(f"Task {task.id} exceeds max_depth ({len(task.depends_on)} > {max_depth})")
        for dep in task.depends_on:
            if dep not in task_ids:
                errors.append(f"Task {task.id} depends on unknown task '{dep}'")
        if task.tool_name in blocked:
            blocked_hits.append(task.tool_name)
        if task.tool_name in disabled:
            disabled_hits.append(task.tool_name)
        if task.tool_name in approval:
            approval_hits.append(task.tool_name)
        if task.side_effect and not allow_side_effects:
            errors.append(f"Task {task.id} uses side effects in a read-only graph")

    valid = not errors and not blocked_hits and not disabled_hits and not approval_hits
    return TaskGraphValidationResult(
        valid=valid,
        errors=errors,
        blocked_tools=sorted(set(blocked_hits)),
        disabled_tools=sorted(set(disabled_hits)),
        approval_required_tools=sorted(set(approval_hits)),
    )