"""ND0603: Detect connections targeting invalid type or index on destination node."""

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("ND0603", Severity.WARNING)


def _parse_input_type_counts(inputs: list[Any]) -> dict[str, int] | None:
    """Parse catalog inputs list into a mapping of connection type to count.

    Returns ``None`` when inputs contain dynamic/expression entries (first
    element is the string ``"="``), meaning validation should be skipped.
    """
    if not inputs:
        return {}

    counts: dict[str, int] = {}
    for entry in inputs:
        if isinstance(entry, str):
            if entry.startswith("="):
                return None
            counts[entry] = counts.get(entry, 0) + 1
        elif isinstance(entry, dict):
            conn_type = entry.get("type")
            if isinstance(conn_type, str):
                counts[conn_type] = counts.get(conn_type, 0) + 1
    return counts


def _build_node_maps(
    workflow: dict[str, Any],
) -> tuple[dict[str, str], set[str]]:
    """Return (node_types name→type, node_names) from workflow nodes."""
    node_types: dict[str, str] = {}
    node_names: set[str] = set()
    for node in workflow.get("nodes", []):
        if not isinstance(node, dict):
            continue
        name = node.get("name")
        ntype = node.get("type")
        if isinstance(name, str):
            node_names.add(name)
            if isinstance(ntype, str):
                node_types[name] = ntype
    return node_types, node_names


def _check_target(
    target: dict[str, Any],
    node_names: set[str],
    node_types: dict[str, str],
) -> Diagnostic | None:
    """Return a diagnostic if the target node rejects the connection type/index."""
    target_node = target.get("node")
    if not isinstance(target_node, str) or target_node not in node_names:
        return None  # ND0602 territory

    node_type = node_types.get(target_node)
    if node_type is None or node_type not in NODE_CATALOG:
        return None  # unknown node, skip

    type_counts = _parse_input_type_counts(NODE_CATALOG[node_type].get("inputs", []))
    if type_counts is None:
        return None  # dynamic inputs, skip

    target_type = target.get("type", "main")
    target_index = target.get("index", 0)

    if target_type not in type_counts:
        return Diagnostic(
            file="",
            location=Location(target_node, "connections"),
            code=_RULE_CODE,
            message=f'Node "{target_node}" does not accept connection type "{target_type}"',
            line=None,
        )

    if isinstance(target_index, int) and target_index >= type_counts[target_type]:
        max_inputs = type_counts[target_type]
        return Diagnostic(
            file="",
            location=Location(target_node, "connections"),
            code=_RULE_CODE,
            message=(
                f'Node "{target_node}" has {max_inputs}'
                f' input(s) of type "{target_type}"'
                f" (index {target_index} out of range)"
            ),
            line=None,
        )

    return None


@register(
    "ND0603",
    Severity.WARNING,
    description="Connection target does not accept connection type/index",
    strict=True,
)
def check_invalid_connection_target(
    workflow: dict[str, Any],
    path: Path,
    source_map: SourceMap = None,
    suggestions: SuggestionLevel = SuggestionLevel.FULL,
) -> list[Diagnostic]:
    """Flag connections whose target node does not accept the given type or index."""
    diagnostics: list[Diagnostic] = []
    fname = str(path)

    node_types, node_names = _build_node_maps(workflow)
    connections = workflow.get("connections", {})

    for source_name, conn_types in connections.items():
        if not isinstance(conn_types, dict):
            continue
        for conn_type, outputs in conn_types.items():
            if not isinstance(outputs, list):
                continue
            for output_idx, output_group in enumerate(outputs):
                if not isinstance(output_group, list):
                    continue
                for target_idx, target in enumerate(output_group):
                    if not isinstance(target, dict):
                        continue
                    diag = _check_target(target, node_names, node_types)
                    if diag is None:
                        continue
                    pointer = f"/connections/{source_name}/{conn_type}/{output_idx}/{target_idx}"
                    diagnostics.append(
                        Diagnostic(
                            file=fname,
                            location=diag.location,
                            code=diag.code,
                            message=diag.message,
                            line=lookup_line(source_map, pointer),
                        ),
                    )

    return diagnostics
