"""Workflow validation orchestrator."""

import json
import sys
from pathlib import Path

from valn8n_lib.rules import RuleFunc, get_rules
from valn8n_lib.rules.io_0001 import IO_READ_FAILED
from valn8n_lib.rules.io_0002 import IO_NOT_OBJECT
from valn8n_lib.rules.io_0010 import IO_EMPTY_FILE
from valn8n_lib.source_map import build_source_map
from valn8n_lib.types import Diagnostic, RuleCode, SuggestionLevel


def discover_files(paths: list[Path]) -> list[Path]:
    """Resolve files and directories to a sorted list of .json files."""
    result: list[Path] = []
    for p in paths:
        if p.is_file():
            result.append(p)
        elif p.is_dir():
            result.extend(sorted(p.rglob("*.json")))
        else:
            print(f"valn8n: error: path not found: {p}", file=sys.stderr)
    return result


def validate_workflow(
    path: Path,
    rules: dict[str, tuple[RuleCode, RuleFunc]],
    suggestions: SuggestionLevel = SuggestionLevel.FULL,
    tolerate_node_keys: frozenset[str] | None = frozenset(),
) -> list[Diagnostic]:
    """Load a JSON workflow file and run all rules against it."""
    if path.stat().st_size == 0:
        return [
            Diagnostic(
                file=str(path),
                location=None,
                code=IO_EMPTY_FILE,
                message="Workflow file is empty (zero bytes)",
            )
        ]

    try:
        raw_text = path.read_text()
        workflow = json.loads(raw_text)
    except (json.JSONDecodeError, OSError) as exc:
        return [
            Diagnostic(
                file=str(path),
                location=None,
                code=IO_READ_FAILED,
                message=f"Failed to read workflow: {exc}",
            )
        ]

    if not isinstance(workflow, dict):
        return [
            Diagnostic(
                file=str(path),
                location=None,
                code=IO_NOT_OBJECT,
                message="Workflow file is not a JSON object",
            )
        ]

    source_map = build_source_map(raw_text)

    diagnostics: list[Diagnostic] = []
    for _rule_code, rule_func in rules.values():
        diagnostics.extend(rule_func(workflow, path, source_map, suggestions))

    # Post-filter ND0201 based on tolerate_node_keys
    if tolerate_node_keys is None:
        # None = wildcard: suppress all ND0201
        diagnostics = [d for d in diagnostics if d.code.code != "ND0201"]
    elif tolerate_node_keys:
        # Non-empty frozenset: suppress ND0201 for specific keys
        diagnostics = [
            d
            for d in diagnostics
            if not (
                d.code.code == "ND0201"
                and d.location is not None
                and d.location.field_path in tolerate_node_keys
            )
        ]

    diagnostics.sort(key=lambda d: (d.line if d.line is not None else float("inf"), d.code.code))

    return diagnostics


def validate_all(
    paths: list[Path],
    select: set[str] | None = None,
    ignore: set[str] | None = None,
    extend_select: set[str] | None = None,
    suggestions: SuggestionLevel = SuggestionLevel.FULL,
    tolerate_node_keys: frozenset[str] | None = frozenset(),
    strict_only: bool = False,
) -> tuple[dict[Path, list[Diagnostic]], list[Path]]:
    """Validate all given paths.

    Returns (results, missing_paths).
    """
    rules = get_rules(select, ignore, extend_select, strict_only=strict_only)
    files = discover_files(paths)

    missing = [p for p in paths if not p.exists()]
    results: dict[Path, list[Diagnostic]] = {}
    for f in files:
        diagnostics = validate_workflow(f, rules, suggestions, tolerate_node_keys)
        results[f] = diagnostics
    return results, missing
