from __future__ import annotations

from app.modules.agent.engine.orchestrator.models import ExecutionPlan, TaskSpec


class PlanValidator:
    def validate(self, plan: ExecutionPlan, task: TaskSpec) -> list[str]:
        errors: list[str] = []
        if not plan.steps:
            errors.append("execution_plan_has_no_steps")
            return errors

        if len(plan.steps) > task.constraints.max_steps:
            errors.append("execution_plan_exceeds_max_steps")

        errors.extend(self._validate_step_ids(plan))
        errors.extend(self._validate_dependencies(plan))
        errors.extend(self._validate_side_effects(plan, task))
        errors.extend(self._validate_step_shape(plan))
        return errors

    def _validate_step_ids(self, plan: ExecutionPlan) -> list[str]:
        seen: set[str] = set()
        out: list[str] = []
        for step in plan.steps:
            if step.step_id in seen:
                out.append(f"duplicate_step_id:{step.step_id}")
            seen.add(step.step_id)
        return out

    def _validate_dependencies(self, plan: ExecutionPlan) -> list[str]:
        out: list[str] = []
        valid_ids = {step.step_id for step in plan.steps}
        for step in plan.steps:
            for dep in step.depends_on:
                if dep not in valid_ids:
                    out.append(f"unknown_dependency:{step.step_id}->{dep}")

        # lightweight cycle detection for directed graph
        graph = {step.step_id: list(step.depends_on) for step in plan.steps}
        visiting: set[str] = set()
        visited: set[str] = set()

        def dfs(node: str) -> bool:
            if node in visiting:
                return True
            if node in visited:
                return False
            visiting.add(node)
            for dep in graph.get(node, []):
                if dfs(dep):
                    return True
            visiting.remove(node)
            visited.add(node)
            return False

        if any(dfs(node) for node in graph):
            out.append("dependency_cycle_detected")
        return out

    def _validate_side_effects(self, plan: ExecutionPlan, task: TaskSpec) -> list[str]:
        if task.constraints.allow_writes:
            return []
        out: list[str] = []
        for step in plan.steps:
            if step.side_effect == "write":
                out.append(f"write_step_not_allowed:{step.step_id}")
        return out

    def _validate_step_shape(self, plan: ExecutionPlan) -> list[str]:
        out: list[str] = []
        for step in plan.steps:
            if step.executor == "graph" and not step.graph_id:
                out.append(f"graph_step_missing_graph_id:{step.step_id}")
            if step.retry.max_attempts < 1:
                out.append(f"invalid_retry_attempts:{step.step_id}")
            if step.timeout_sec < 1:
                out.append(f"invalid_step_timeout:{step.step_id}")
        return out
