"""ND0302: Parameter value type validation."""

from pathlib import Path
from typing import Any

from valn8n_lib.rules._catalog import NODE_CATALOG, build_prop_map, resolve_property_defs
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("ND0302", Severity.ERROR)


def _is_n8n_expression(value: Any) -> bool:
    """Check if a value is an n8n expression (must start with '={{')."""
    return isinstance(value, str) and value.startswith("={{")


def _check_param_type(value: Any, prop: dict[str, Any]) -> str | None:  # noqa: PLR0911, PLR0912
    """Check a parameter value against its catalog property type. Returns error or None."""
    prop_type = prop.get("type", "")

    # multipleValues: n8n stores an array of the declared type
    type_options = prop.get("typeOptions") or {}
    if type_options.get("multipleValues") and isinstance(value, list):
        return None

    match prop_type:
        case "string":
            if not isinstance(value, str):
                return f"expected string, got {type(value).__name__}"

        case "number":
            if isinstance(value, bool) or not isinstance(value, (int, float)):
                return f"expected number, got {type(value).__name__}"

        case "boolean":
            if not isinstance(value, bool):
                return f"expected boolean, got {type(value).__name__}"

        case "options":
            # n8n option values can be string | number | boolean
            type_options = prop.get("typeOptions") or {}
            has_dynamic = bool(
                type_options.get("loadOptionsMethod") or type_options.get("loadOptionsDependsOn")
            )
            options = prop.get("options") or []
            valid_values = [o["value"] for o in options if isinstance(o, dict) and "value" in o]

            if has_dynamic or not valid_values:
                # Dynamic or unknown options: accept any scalar
                if not isinstance(value, (str, int, float, bool)):
                    return f"expected scalar (option), got {type(value).__name__}"
            # Static options: validate value is in the list
            elif value not in valid_values:
                valid_str = ", ".join(repr(v) for v in valid_values)
                return f"invalid option {value!r}; valid values: {valid_str}"

        case "multiOptions":
            if not isinstance(value, list):
                return f"expected array, got {type(value).__name__}"
            options = prop.get("options", [])
            if options:
                valid_set = {o["value"] for o in options if isinstance(o, dict) and "value" in o}
                if valid_set:
                    for item in value:
                        if item not in valid_set:
                            return f"invalid multiOptions value {item!r}"

        case "json":
            if not isinstance(value, str):
                return f"expected string (JSON), got {type(value).__name__}"

        case "resourceLocator":
            # n8n stores resourceLocator as either {"mode":..., "value":...} or a plain scalar
            if not isinstance(value, (dict, str, int, float, bool)):
                return f"expected object or scalar, got {type(value).__name__}"

        case (
            "collection" | "fixedCollection" | "resourceMapper" | "assignmentCollection" | "filter"
        ):
            if not isinstance(value, dict):
                return f"expected object, got {type(value).__name__}"

        case _:
            pass

    return None


@register("ND0302", Severity.ERROR, description="Wrong type for parameter value")
def check_param_value_types(
    workflow: dict[str, Any],
    path: Path,
    source_map: SourceMap = None,
    suggestions: SuggestionLevel = SuggestionLevel.FULL,
) -> list[Diagnostic]:
    """Flag parameter values whose types don't match the node catalog."""
    diagnostics: list[Diagnostic] = []
    nodes = workflow.get("nodes", [])
    file = str(path)

    for node_index, node in enumerate(nodes):
        if not isinstance(node, dict):
            continue

        node_type = node.get("type", "")
        node_name = node.get("name", "<unnamed>")
        parameters = node.get("parameters")
        if not isinstance(parameters, dict):
            continue

        catalog_entry = NODE_CATALOG.get(node_type)
        if catalog_entry is None:
            continue

        property_defs = resolve_property_defs(catalog_entry, node.get("typeVersion"))
        if property_defs is None:
            continue

        prop_map = build_prop_map(property_defs)

        for param_name, param_value in parameters.items():
            if _is_n8n_expression(param_value):
                continue

            matched_prop = prop_map.get(param_name)
            if matched_prop is None:
                continue

            error = _check_param_type(param_value, matched_prop)
            if error is not None:
                pointer = f"/nodes/{node_index}/parameters/{param_name}"
                line = lookup_line(source_map, pointer)
                diagnostics.append(
                    Diagnostic(
                        file=file,
                        location=Location(node_name, f"parameters/{param_name}"),
                        code=_RULE_CODE,
                        message=f'Wrong type for parameter "{param_name}": {error}',
                        line=line,
                    )
                )

    return diagnostics
