#!/usr/bin/env python3
"""Segnala stati irraggiungibili, uscite mancanti e cicli senza termine."""

from __future__ import annotations

import argparse
import json
import sys
from collections import defaultdict, deque
from pathlib import Path
from typing import Any


def strongly_connected_components(graph: dict[str, set[str]]) -> list[list[str]]:
    index = 0
    stack: list[str] = []
    on_stack: set[str] = set()
    indices: dict[str, int] = {}
    lowlinks: dict[str, int] = {}
    components: list[list[str]] = []

    def visit(node: str) -> None:
        nonlocal index
        indices[node] = index
        lowlinks[node] = index
        index += 1
        stack.append(node)
        on_stack.add(node)
        for target in graph.get(node, set()):
            if target not in graph:
                continue
            if target not in indices:
                visit(target)
                lowlinks[node] = min(lowlinks[node], lowlinks[target])
            elif target in on_stack:
                lowlinks[node] = min(lowlinks[node], indices[target])
        if lowlinks[node] == indices[node]:
            component: list[str] = []
            while True:
                current = stack.pop()
                on_stack.remove(current)
                component.append(current)
                if current == node:
                    break
            components.append(sorted(component))

    for node in sorted(graph):
        if node not in indices:
            visit(node)
    return components


def analyze_graph(data: dict[str, Any]) -> dict[str, Any]:
    states = [state for state in data.get("states", []) if isinstance(state, dict)]
    state_map = {
        state["id"]: state
        for state in states
        if isinstance(state.get("id"), str) and state["id"]
    }
    graph: dict[str, set[str]] = {
        state_id: {
            target
            for target in state.get("next", [])
            if isinstance(target, str)
        }
        for state_id, state in state_map.items()
    }
    start = data.get("trigger", {}).get("start_state") if isinstance(data.get("trigger"), dict) else None

    missing_targets = sorted(
        {target for targets in graph.values() for target in targets if target not in state_map}
    )

    reachable: set[str] = set()
    if start in state_map:
        queue = deque([start])
        while queue:
            node = queue.popleft()
            if node in reachable:
                continue
            reachable.add(node)
            queue.extend(target for target in graph[node] if target in state_map)

    unreachable = sorted(set(state_map) - reachable)
    missing_exits = sorted(
        state_id
        for state_id, state in state_map.items()
        if not graph[state_id] and state.get("type") != "end"
    )

    terminal_states = {
        state_id
        for state_id, state in state_map.items()
        if state.get("type") == "end"
    }
    reverse_graph: dict[str, set[str]] = defaultdict(set)
    for source, targets in graph.items():
        for target in targets:
            if target in state_map:
                reverse_graph[target].add(source)
    can_reach_terminal = set(terminal_states)
    queue = deque(terminal_states)
    while queue:
        node = queue.popleft()
        for source in reverse_graph[node]:
            if source not in can_reach_terminal:
                can_reach_terminal.add(source)
                queue.append(source)

    terminal_free_cycles: list[list[str]] = []
    for component in strongly_connected_components(graph):
        is_cycle = len(component) > 1 or any(node in graph[node] for node in component)
        has_explicit_stop = any(state_map[node].get("termination_condition") for node in component)
        if is_cycle and not has_explicit_stop and not any(node in can_reach_terminal for node in component):
            terminal_free_cycles.append(component)

    return {
        "start_state": start,
        "unreachable_states": unreachable,
        "missing_targets": missing_targets,
        "non_final_states_without_exit": missing_exits,
        "cycles_without_terminal_path": terminal_free_cycles,
    }


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("manifest", type=Path)
    parser.add_argument("--json", action="store_true", dest="as_json")
    args = parser.parse_args()
    try:
        data = json.loads(args.manifest.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as exc:
        print(f"Impossibile leggere il manifest: {exc}", file=sys.stderr)
        return 2
    if not isinstance(data, dict):
        print("La radice deve essere un oggetto JSON.", file=sys.stderr)
        return 2
    result = analyze_graph(data)
    has_findings = any(
        result[key]
        for key in (
            "unreachable_states",
            "missing_targets",
            "non_final_states_without_exit",
            "cycles_without_terminal_path",
        )
    )
    if args.as_json:
        print(json.dumps(result, ensure_ascii=False, indent=2))
    else:
        print(f"Stato iniziale: {result['start_state'] or 'non definito'}")
        labels = {
            "unreachable_states": "Stati irraggiungibili",
            "missing_targets": "Destinazioni inesistenti",
            "non_final_states_without_exit": "Stati non finali senza uscita",
            "cycles_without_terminal_path": "Cicli senza percorso terminale",
        }
        for key, label in labels.items():
            value = result[key]
            print(f"{label}: {value if value else 'nessuno'}")
    return 1 if has_findings else 0


if __name__ == "__main__":
    raise SystemExit(main())
