"""ND0601: Detect nodes with no incoming connections that should have them."""

from pathlib import Path
from typing import Any

from valn8n_lib.rules._catalog import NODE_CATALOG
from valn8n_lib.rules.registry import SourceMap, register
from valn8n_lib.source_map import lookup_line
from valn8n_lib.types import Diagnostic, Location, RuleCode, Severity, SuggestionLevel

_RULE_CODE = RuleCode("ND0601", Severity.WARNING)


def _is_trigger_type(node_type: str) -> bool:
    """Check if node type is a trigger (catalog lookup or name heuristic)."""
    entry = NODE_CATALOG.get(node_type)
    if entry is not None:
        return bool(entry.get("has_trigger"))
    # Fallback for unknown types: check name
    return "trigger" in node_type.lower()


def _has_no_inputs(node_type: str) -> bool:
    """Check if a catalog node has an empty inputs list (e.g. stickyNote)."""
    entry = NODE_CATALOG.get(node_type)
    if entry is not None:
        return entry.get("inputs") == []
    return False


def _build_connection_sets(
    connections: dict[str, Any],
) -> tuple[set[str], set[str]]:
    """Return (destination_nodes, ai_source_nodes) from connections dict."""
    destination_nodes: set[str] = set()
    ai_source_nodes: set[str] = set()
    for source_name, conn_types in connections.items():
        if not isinstance(conn_types, dict):
            continue
        for conn_type, outputs in conn_types.items():
            if conn_type.startswith("ai_"):
                ai_source_nodes.add(source_name)
            if not isinstance(outputs, list):
                continue
            for output_group in outputs:
                if not isinstance(output_group, list):
                    continue
                for target in output_group:
                    if isinstance(target, dict) and "node" in target:
                        destination_nodes.add(target["node"])
    return destination_nodes, ai_source_nodes


def _is_exempt(
    node: dict[str, Any], destination_nodes: set[str], ai_source_nodes: set[str]
) -> bool:
    """Check if a node is exempt from ND0601."""
    node_name = node.get("name", "")
    node_type = node.get("type", "")
    return (
        node_name in destination_nodes
        or bool(node.get("disabled"))
        or _is_trigger_type(node_type)
        or _has_no_inputs(node_type)
        or node_name in ai_source_nodes
    )


@register(
    "ND0601", Severity.WARNING, description="Input not connected in non-trigger node", strict=True
)
def check_unconnected_node(
    workflow: dict[str, Any],
    path: Path,
    source_map: SourceMap = None,
    suggestions: SuggestionLevel = SuggestionLevel.FULL,
) -> list[Diagnostic]:
    """Flag nodes that have no incoming connections and are not exempt."""
    diagnostics: list[Diagnostic] = []
    fname = str(path)
    connections = workflow.get("connections", {})
    destination_nodes, ai_source_nodes = _build_connection_sets(connections)

    for i, node in enumerate(workflow.get("nodes", [])):
        if _is_exempt(node, destination_nodes, ai_source_nodes):
            continue
        node_name = node.get("name", f"node[{i}]")
        pointer = f"/nodes/{i}/name"
        line = lookup_line(source_map, pointer)
        diagnostics.append(
            Diagnostic(
                file=fname,
                location=Location(node_name, "connections"),
                code=_RULE_CODE,
                message=f'Node "{node_name}" has no incoming connections',
                line=line,
            ),
        )

    return diagnostics
