"""DAG resolution for greenfield dependencies.

Builds a directed graph from dependency entries, detects cycles,
deduplicates, and returns a topologically sorted list (dependencies
before dependents).
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

from complyform.core.errors import DependencyCycleError


@dataclass(frozen=True)
class DependencySpec:
    """A resolved dependency that must be generated."""

    template_name: str  # e.g., "kms_key_ring", "kms_crypto_key_cloudsql"
    resource_type: str  # e.g., "google_kms_key_ring"
    required_by: tuple[str, ...]  # resource types that depend on this


def resolve_dependencies(
    dependency_entries: list[dict[str, Any]],
) -> list[DependencySpec]:
    """Build DAG from dependency entries. Detect cycles. Deduplicate.

    Each entry must have: resource_type, template, and optionally required_by.
    Returns topologically sorted list (dependencies before dependents).
    """
    if not dependency_entries:
        return []

    # Build deduplicated registry: template → (resource_type, required_by set)
    registry: dict[str, _DepNode] = {}
    for entry in dependency_entries:
        template = entry["template"]
        resource_type = entry["resource_type"]
        required_by = entry.get("required_by", "")

        if template not in registry:
            registry[template] = _DepNode(
                template=template,
                resource_type=resource_type,
                required_by=set(),
                depends_on=set(),
            )
        if required_by:
            registry[template].required_by.add(required_by)

    # Build adjacency: template → templates it depends on
    # Convention: if template "B" is a dependency of template "A",
    # we look for B's resource_type in A's depends_on entries
    _build_adjacency(registry, dependency_entries)

    # Topological sort with cycle detection
    sorted_templates = _topological_sort(registry)

    return [
        DependencySpec(
            template_name=t,
            resource_type=registry[t].resource_type,
            required_by=tuple(sorted(registry[t].required_by)),
        )
        for t in sorted_templates
    ]


@dataclass
class _DepNode:
    """Internal node for dependency graph building."""

    template: str
    resource_type: str
    required_by: set[str]
    depends_on: set[str] = field(default_factory=set)


def _build_adjacency(
    registry: dict[str, _DepNode],
    entries: list[dict[str, Any]],
) -> None:
    """Build depends_on edges from dependency entries.

    If entry has a 'depends_on' field listing other templates, use it.
    """
    for entry in entries:
        template = entry["template"]
        deps = entry.get("depends_on", [])
        if isinstance(deps, list):
            for dep in deps:
                if dep in registry:
                    registry[template].depends_on.add(dep)


def _topological_sort(registry: dict[str, _DepNode]) -> list[str]:
    """Kahn's algorithm with cycle detection."""
    # Build in-degree counts
    in_degree: dict[str, int] = {t: 0 for t in registry}
    for node in registry.values():
        for dep in node.depends_on:
            if dep in in_degree:
                in_degree[dep] = in_degree.get(dep, 0)
                # dep is depended on by node.template — but we want
                # dep to come first, so node has an edge FROM dep
                pass

    # Correct approach: edge from dep → template means dep must come first
    # So in-degree of template increases for each of its depends_on
    in_degree = {t: 0 for t in registry}
    adj: dict[str, list[str]] = {t: [] for t in registry}
    for template, node in registry.items():
        for dep in node.depends_on:
            if dep in registry:
                adj[dep].append(template)
                in_degree[template] += 1

    # Start with nodes that have no dependencies
    queue = [t for t in registry if in_degree[t] == 0]
    queue.sort()  # deterministic ordering
    result: list[str] = []

    while queue:
        current = queue.pop(0)
        result.append(current)
        for neighbor in sorted(adj[current]):
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)
        queue.sort()

    if len(result) != len(registry):
        # Cycle detected — find it
        remaining = set(registry) - set(result)
        cycle = _find_cycle(adj, remaining)
        raise DependencyCycleError(cycle)

    return result


def _find_cycle(adj: dict[str, list[str]], nodes: set[str]) -> list[str]:
    """Find a cycle in the remaining unvisited nodes."""
    visited: set[str] = set()
    path: list[str] = []
    path_set: set[str] = set()

    def dfs(node: str) -> list[str] | None:
        if node in path_set:
            idx = path.index(node)
            return [*path[idx:], node]
        if node in visited:
            return None
        visited.add(node)
        path.append(node)
        path_set.add(node)
        for neighbor in adj.get(node, []):
            if neighbor in nodes:
                result = dfs(neighbor)
                if result is not None:
                    return result
        path.pop()
        path_set.discard(node)
        return None

    for node in sorted(nodes):
        result = dfs(node)
        if result is not None:
            return result

    return list(nodes)  # fallback
