"""ND0301: Deep parameter key validation."""

import json
from importlib.resources import files
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,
    format_suggestions,
)

# Load parameter schemas
_schema_text = files("valn8n_lib.data").joinpath("parameter_schemas.json").read_text()
_schema_data = json.loads(_schema_text)
SCHEMAS: dict[str, Any] = _schema_data["schemas"]
TYPE_TO_SCHEMA: dict[str, str] = _schema_data["type_to_schema"]

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


def _is_expression(value: Any) -> bool:
    """Check if a value is an expression string that can't be validated statically."""
    return isinstance(value, str)


def _validate_schema(
    value: Any,
    *,
    schema_name: str,
    json_path: str,
    node_name: str,
    node_index: int,
    source_map: SourceMap,
    suggestions: SuggestionLevel,
    diagnostics: list[Diagnostic],
    file: str,
) -> None:
    """Recursively validate a value against a named parameter schema."""
    if not isinstance(value, dict):
        return

    schema = SCHEMAS.get(schema_name)
    if schema is None:
        return

    valid_keys: set[str] = set(schema["keys"])
    nested: dict[str, Any] = schema.get("nested", {})
    suffix = format_suggestions(valid_keys, suggestions)

    for key in value:
        if key not in valid_keys:
            pointer = f"/nodes/{node_index}/parameters{json_path}/{key}"
            line = lookup_line(source_map, pointer)
            diagnostics.append(
                Diagnostic(
                    file=file,
                    location=Location(node_name, f"parameters{json_path}/{key}"),
                    code=_RULE_CODE,
                    message=f'Unknown parameter key "{key}" in {schema_name}' + suffix,
                    line=line,
                )
            )

        if key in nested:
            child_info = nested[key]
            child_schema = child_info["schema"]
            child_value = value[key]
            child_path = f"{json_path}/{key}"

            if child_info.get("array") and isinstance(child_value, list):
                for i, item in enumerate(child_value):
                    if not _is_expression(item):
                        _validate_schema(
                            item,
                            schema_name=child_schema,
                            json_path=f"{child_path}/{i}",
                            node_name=node_name,
                            node_index=node_index,
                            source_map=source_map,
                            suggestions=suggestions,
                            diagnostics=diagnostics,
                            file=file,
                        )
            elif not _is_expression(child_value):
                _validate_schema(
                    child_value,
                    schema_name=child_schema,
                    json_path=child_path,
                    node_name=node_name,
                    node_index=node_index,
                    source_map=source_map,
                    suggestions=suggestions,
                    diagnostics=diagnostics,
                    file=file,
                )


def _validate_collection(
    value: Any,
    options: list[dict[str, Any]],
    *,
    json_path: str,
    node_name: str,
    node_index: int,
    source_map: SourceMap,
    suggestions: SuggestionLevel,
    diagnostics: list[Diagnostic],
    file: str,
) -> None:
    """Validate keys in a collection parameter against its option names."""
    if not isinstance(value, dict):
        return

    valid_keys: set[str] = set()
    for opt in options:
        if not isinstance(opt, dict):
            continue
        name = opt.get("name")
        if name is None:
            # Incomplete option list (e.g. spread syntax not extracted) — skip validation
            return
        valid_keys.add(name)
    if not valid_keys:
        return

    suffix = format_suggestions(valid_keys, suggestions)
    for key in value:
        if key not in valid_keys:
            pointer = f"/nodes/{node_index}/parameters{json_path}/{key}"
            line = lookup_line(source_map, pointer)
            diagnostics.append(
                Diagnostic(
                    file=file,
                    location=Location(node_name, f"parameters{json_path}/{key}"),
                    code=_RULE_CODE,
                    message=f'Unknown parameter key "{key}" in collection' + suffix,
                    line=line,
                )
            )


def _validate_fixed_collection(  # noqa: PLR0912
    value: Any,
    options: list[dict[str, Any]],
    *,
    json_path: str,
    node_name: str,
    node_index: int,
    source_map: SourceMap,
    suggestions: SuggestionLevel,
    diagnostics: list[Diagnostic],
    file: str,
) -> None:
    """Validate keys in a fixedCollection parameter."""
    if not isinstance(value, dict):
        return

    # Top-level keys must be option names
    option_map: dict[str, list[dict[str, Any]]] = {}
    for opt in options:
        if not isinstance(opt, dict):
            continue
        name = opt.get("name")
        if name is None:
            # Incomplete option list — skip validation
            return
        if name:
            option_map[name] = opt.get("values", [])

    valid_keys = set(option_map.keys())
    if not valid_keys:
        return

    suffix = format_suggestions(valid_keys, suggestions)
    for key in value:
        if key not in valid_keys:
            pointer = f"/nodes/{node_index}/parameters{json_path}/{key}"
            line = lookup_line(source_map, pointer)
            diagnostics.append(
                Diagnostic(
                    file=file,
                    location=Location(node_name, f"parameters{json_path}/{key}"),
                    code=_RULE_CODE,
                    message=f'Unknown parameter key "{key}" in fixedCollection' + suffix,
                    line=line,
                )
            )
            continue

        # Validate sub-keys within each option group
        sub_props = option_map[key]
        if not sub_props:
            continue

        sub_valid: set[str] = {sp["name"] for sp in sub_props if "name" in sp}
        if not sub_valid:
            continue

        child_value = value[key]
        items: list[tuple[int | None, Any]] = []
        if isinstance(child_value, list):
            items = [(i, item) for i, item in enumerate(child_value)]
        elif isinstance(child_value, dict):
            items = [(None, child_value)]

        sub_suffix = format_suggestions(sub_valid, suggestions)
        for idx, item in items:
            if not isinstance(item, dict):
                continue
            item_path = f"{json_path}/{key}/{idx}" if idx is not None else f"{json_path}/{key}"
            for sub_key in item:
                if sub_key not in sub_valid:
                    pointer = f"/nodes/{node_index}/parameters{item_path}/{sub_key}"
                    line = lookup_line(source_map, pointer)
                    diagnostics.append(
                        Diagnostic(
                            file=file,
                            location=Location(node_name, f"parameters{item_path}/{sub_key}"),
                            code=_RULE_CODE,
                            message=(
                                f'Unknown parameter key "{sub_key}"'
                                f' in fixedCollection group "{key}"'
                            )
                            + sub_suffix,
                            line=line,
                        )
                    )


@register("ND0301", Severity.WARNING, description="Unknown nested parameter key")
def check_deep_parameter_keys(  # noqa: PLR0912
    workflow: dict[str, Any],
    path: Path,
    source_map: SourceMap = None,
    suggestions: SuggestionLevel = SuggestionLevel.FULL,
) -> list[Diagnostic]:
    """Flag unknown keys at any nesting depth inside node parameters."""
    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

        # Look up node type in catalog
        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_expression(param_value):
                continue

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

            prop_type = matched_prop.get("type", "")
            json_path = f"/{param_name}"

            if prop_type in TYPE_TO_SCHEMA:
                schema_name = TYPE_TO_SCHEMA[prop_type]
                _validate_schema(
                    param_value,
                    schema_name=schema_name,
                    json_path=json_path,
                    node_name=node_name,
                    node_index=node_index,
                    source_map=source_map,
                    suggestions=suggestions,
                    diagnostics=diagnostics,
                    file=file,
                )
            elif prop_type == "collection":
                prop_options = matched_prop.get("options", [])
                if isinstance(prop_options, list):
                    _validate_collection(
                        param_value,
                        prop_options,
                        json_path=json_path,
                        node_name=node_name,
                        node_index=node_index,
                        source_map=source_map,
                        suggestions=suggestions,
                        diagnostics=diagnostics,
                        file=file,
                    )
            elif prop_type == "fixedCollection":
                prop_options = matched_prop.get("options", [])
                if isinstance(prop_options, list):
                    _validate_fixed_collection(
                        param_value,
                        prop_options,
                        json_path=json_path,
                        node_name=node_name,
                        node_index=node_index,
                        source_map=source_map,
                        suggestions=suggestions,
                        diagnostics=diagnostics,
                        file=file,
                    )

    return diagnostics
