"""Analyze whether state definitions are uniquely identifiable by field signatures.

This script is intentionally placed under scripts/ (but is NOT a pytest test module)
so it can be used as a repeatable, versioned check.

It expands each state's possible value set for:
  (binding_status, action, status) and optionally data_id_exist.

Wildcard semantics:
- '*' means "any enum value" for enum fields.
- list means "any of these values".

For data_id_exist (optional):
- If present: bool | '*' | [bool, ...]
- If missing: treated as unknown by default, or as wildcard with --data-id-missing-wildcard.

Usage:
    python scripts/analyze_state_signatures.py
    python scripts/analyze_state_signatures.py --with-data-id
    python scripts/analyze_state_signatures.py --fail-on-overlap
"""

from __future__ import annotations

import argparse
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Set, Tuple

import yaml


REPO_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_CFG = REPO_ROOT / "sync_state_machine" / "config" / "node_state_machine.yaml"

# Ensure repository root is importable when running as a script.
if str(REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(REPO_ROOT))


@dataclass(frozen=True)
class Collision:
    signature: Tuple[Any, ...]
    states: Tuple[str, ...]


def _load_yaml(path: Path) -> Dict[str, Any]:
    with path.open("r", encoding="utf-8") as f:
        data = yaml.safe_load(f)
    if not isinstance(data, dict):
        raise TypeError(f"Top-level YAML must be a mapping: {path}")
    return data


def _import_enum_names() -> Tuple[Set[str], Set[str], Set[str]]:
    # Validate enum NAMES; values are implementation details.
    from sync_state_machine.common.types import BindingStatus, SyncAction, SyncStatus

    binding = {e.name for e in BindingStatus}
    action = {e.name for e in SyncAction}
    status = {e.name for e in SyncStatus}
    return binding, action, status


def _expand_enum_field(value: Any, *, all_values: Set[str], field_path: str) -> Set[str]:
    if isinstance(value, str):
        if value == "*":
            return set(all_values)
        return {value}
    if isinstance(value, list):
        expanded: Set[str] = set()
        for i, item in enumerate(value):
            if not isinstance(item, str):
                raise TypeError(f"{field_path}[{i}] must be a string")
            if item == "*":
                expanded |= set(all_values)
            else:
                expanded.add(item)
        return expanded
    raise TypeError(f"{field_path} must be str | list[str]")


def _expand_data_id_exist(
    value: Any,
    *,
    field_path: str,
    missing_as_wildcard: bool,
) -> Set[Optional[bool]]:
    if value is None:
        return {True, False} if missing_as_wildcard else {None}
    if value == "*":
        return {True, False}
    if isinstance(value, bool):
        return {value}
    if isinstance(value, list):
        out: Set[Optional[bool]] = set()
        for i, item in enumerate(value):
            if item == "*":
                out |= {True, False}
            elif isinstance(item, bool):
                out.add(item)
            else:
                raise TypeError(f"{field_path}[{i}] must be bool or '*'")
        return out
    raise TypeError(f"{field_path} must be bool | '*' | list[bool]")


def _cartesian_product(*sets: Sequence[Any]) -> Iterable[Tuple[Any, ...]]:
    if not sets:
        return []
    acc: List[Tuple[Any, ...]] = [()]
    for s in sets:
        nxt: List[Tuple[Any, ...]] = []
        for prefix in acc:
            for v in s:
                nxt.append(prefix + (v,))
        acc = nxt
    return acc


def _collect_state_signature_sets(
    cfg: Mapping[str, Any],
    *,
    with_data_id: bool,
    data_id_missing_as_wildcard: bool,
) -> Dict[str, Set[Tuple[Any, ...]]]:
    states = cfg.get("states")
    if not isinstance(states, dict):
        raise TypeError("cfg.states must be a mapping")

    binding_all, action_all, status_all = _import_enum_names()

    out: Dict[str, Set[Tuple[Any, ...]]] = {}
    for sid, sdef in states.items():
        if not isinstance(sdef, dict):
            continue

        binding = _expand_enum_field(sdef.get("binding_status"), all_values=binding_all, field_path=f"states.{sid}.binding_status")
        action = _expand_enum_field(sdef.get("action"), all_values=action_all, field_path=f"states.{sid}.action")
        status = _expand_enum_field(sdef.get("status"), all_values=status_all, field_path=f"states.{sid}.status")

        if with_data_id:
            data_id = _expand_data_id_exist(
                sdef.get("data_id_exist"),
                field_path=f"states.{sid}.data_id_exist",
                missing_as_wildcard=data_id_missing_as_wildcard,
            )
            signatures = set(_cartesian_product(sorted(binding), sorted(action), sorted(status), sorted(data_id, key=lambda x: str(x))))
        else:
            signatures = set(_cartesian_product(sorted(binding), sorted(action), sorted(status)))

        out[str(sid)] = signatures

    return out


def _find_collisions(state_signatures: Mapping[str, Set[Tuple[Any, ...]]]) -> List[Collision]:
    index: Dict[Tuple[Any, ...], List[str]] = {}
    for sid, sigs in state_signatures.items():
        for sig in sigs:
            index.setdefault(sig, []).append(sid)

    collisions: List[Collision] = []
    for sig, sids in index.items():
        if len(sids) > 1:
            collisions.append(Collision(signature=sig, states=tuple(sorted(sids))))

    collisions.sort(key=lambda c: (-len(c.states), c.signature))
    return collisions


def main(argv: Optional[Sequence[str]] = None) -> int:
    parser = argparse.ArgumentParser(description="Analyze state signature overlaps")
    parser.add_argument("--config", default=str(DEFAULT_CFG), help="Path to node_state_machine.yaml")
    parser.add_argument("--with-data-id", action="store_true", help="Include data_id_exist in signature")
    parser.add_argument(
        "--data-id-missing-wildcard",
        action="store_true",
        help="If data_id_exist is missing, treat it as wildcard (True/False) instead of unknown",
    )
    parser.add_argument("--fail-on-overlap", action="store_true", help="Exit non-zero if collisions exist")
    parser.add_argument("--show", type=int, default=30, help="Max collisions to print")
    args = parser.parse_args(list(argv) if argv is not None else None)

    cfg = _load_yaml(Path(args.config))
    state_sigs = _collect_state_signature_sets(
        cfg,
        with_data_id=bool(args.with_data_id),
        data_id_missing_as_wildcard=bool(args.data_id_missing_wildcard),
    )
    collisions = _find_collisions(state_sigs)

    dim = 4 if args.with_data_id else 3
    print(f"Config: {args.config}")
    print(f"Signature dims: {dim} ({'binding,action,status,data_id' if args.with_data_id else 'binding,action,status'})")
    print(f"States analyzed: {len(state_sigs)}")
    print(f"Collisions: {len(collisions)}")

    if collisions:
        print("\nTop collisions:")
        for c in collisions[: args.show]:
            print(f"  {c.signature} -> {', '.join(c.states)}")

    if args.fail_on_overlap and collisions:
        return 2
    return 0


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