# src/xstate_statemachine/cli/__main__.py
# -----------------------------------------------------------------------------
# 🏛️ Command-Line Interface (CLI) - Main Entry Point
# -----------------------------------------------------------------------------
# This module serves as the main entry point for the xstate-statemachine CLI.
# It orchestrates the code generation process by following a clean,
# sequential workflow:
#
#   1. ⚙️ Parse and validate command-line arguments.
#   2. 📂 Load, parse, and validate the input JSON machine definitions.
#   3. 🤝 Determine machine hierarchy, either automatically or interactively.
#   4. 📜 Generate the state machine logic and runner code in memory.
#   5. 💾 Write the generated code to the appropriate files on disk.
#
# The logic is decomposed into small, single-responsibility helper functions
# to enhance modularity, testability, and maintainability, following SOLID
# and DRY principles.
# -----------------------------------------------------------------------------

# -----------------------------------------------------------------------------
# 📦 Standard Library Imports
# -----------------------------------------------------------------------------
import argparse
import json
import logging
from pathlib import Path
from typing import Any, Dict, List, Set, Tuple

# -----------------------------------------------------------------------------
# 📥 Project-Specific Imports
# -----------------------------------------------------------------------------
# Assuming these modules exist in the same directory (`.`) as per the original.
from .args import (
    get_parser,
    resolve_async_mode,
    resolve_template,
    validate_args,
)
from .extractor import extract_logic_names, guess_hierarchy
from .strategies import GenerationContext, get_strategy
from .utils import camel_to_snake, normalize_bool

# -----------------------------------------------------------------------------
# 🪵 Logger Configuration
# -----------------------------------------------------------------------------
# A single logger instance for consistent logging across the module.
logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)


# -----------------------------------------------------------------------------
# 📁 Path & File Helpers
# -----------------------------------------------------------------------------
# Functions responsible for handling file paths, reading from, and writing to
# the filesystem. Each has a distinct, single responsibility.
# -----------------------------------------------------------------------------


def _get_validated_json_paths(
    args: argparse.Namespace, parser: argparse.ArgumentParser
) -> List[str]:
    """Collects, de-duplicates, and validates all input JSON file paths.

    It respects the hierarchy flags (--json-parent, --json-child) to establish
    an initial order before de-duplicating.

    Args:
        args (argparse.Namespace): The parsed command-line arguments.
        parser (argparse.ArgumentParser): The parser instance for error reporting.

    Returns:
        List[str]: An ordered, unique list of validated JSON file paths.

    Raises:
        SystemExit: If no JSON files are provided or if a child is specified
                    without a parent.
    """
    logger.info("📂 Validating and collecting input file paths...")
    # 🗂️ Collect paths in order: parent, standard, children
    json_paths: List[str] = []
    if args.json_parent:
        json_paths.append(args.json_parent)
    json_paths.extend(args.json)
    json_paths.extend(args.json_files)
    json_paths.extend(args.json_child)

    # 🛑 Validate that a child machine is not specified without a parent
    has_children = bool(args.json_child)
    has_parent = bool(args.json_parent or args.json or args.json_files)
    if has_children and not has_parent:
        err_msg = "--json-child requires a parent machine JSON."
        logger.error(f"❌ {err_msg}")
        parser.error(err_msg)

    # 🔄 De-duplicate paths while preserving the specified order
    seen: Set[str] = set()
    unique_paths = [p for p in json_paths if not (p in seen or seen.add(p))]

    if not unique_paths:
        logger.error("❌ No JSON files provided. Aborting.")
        parser.error("At least one JSON file is required.")

    logger.info(
        f"✅ Found {len(unique_paths)} unique JSON file(s) to process."
    )
    return unique_paths


def _determine_output_paths(
    args: argparse.Namespace,
    machine_names: List[str],
    hierarchy_flag: bool,
    json_paths: List[str],
) -> Dict[str, Path]:
    """Determines and creates the output directory and file paths.

    Args:
        args (argparse.Namespace): Parsed command-line arguments.
        machine_names (List[str]): List of snake_cased machine names.
        hierarchy_flag (bool): True if generating a hierarchical machine.
        json_paths (List[str]): List of input file paths to infer output dir.

    Returns:
        Dict[str, Path]: A dictionary mapping file types to their output Paths.
    """
    logger.info("🗺️ Determining output file paths...")
    # 📁 Define output directory (or infer it from the first JSON path)
    out_dir = Path(args.output) if args.output else Path(json_paths[0]).parent
    out_dir.mkdir(parents=True, exist_ok=True)

    # 📝 Determine the base filename for the output files
    if hierarchy_flag and len(machine_names) > 1:
        base_name = machine_names[0]  # Parent name
    else:
        base_name = "_".join(machine_names)

    paths = {
        "logic_file": out_dir / f"{base_name}_logic.py",
        "runner_file": out_dir / f"{base_name}_runner.py",
        "single_file": out_dir / f"{base_name}.py",
    }
    logger.info(f"✅ Output directory set to: {out_dir.resolve()}")
    return paths


def _safe_print(msg: str) -> None:
    """Print a message safely, handling encoding errors on Windows.

    On Windows with non-UTF-8 console encodings (e.g. cp1252), emoji
    characters cause UnicodeEncodeError.  This helper falls back to
    ASCII-safe output when that happens.
    """
    try:
        print(msg)
    except UnicodeEncodeError:
        print(msg.encode("ascii", errors="replace").decode("ascii"))


def _write_output_files(
    file_count: int,
    paths: Dict[str, Path],
    logic_code: str,
    runner_code: str,
) -> None:
    """Writes the generated code strings to the appropriate files.

    Args:
        file_count (int): The number of files to generate (1 or 2).
        paths (Dict[str, Path]): A dictionary of output file paths.
        logic_code (str): The generated logic code.
        runner_code (str): The generated runner code.
    """
    logger.info("✍️ Writing generated code to disk...")
    if file_count == 1:
        # 🤝 Merge code into a single file
        combined_code = _merge_code_for_single_file(logic_code, runner_code)
        target_path = paths["single_file"]
        logger.info(f"💾 Writing combined code to: {target_path}")
        target_path.write_text(combined_code, encoding="utf-8")
        _safe_print(f"Generated combined file: {target_path}")
    else:
        # ✌️ Write to separate logic and runner files
        logic_path, runner_path = paths["logic_file"], paths["runner_file"]
        logger.info(f"💾 Writing logic code to: {logic_path}")
        logic_path.write_text(logic_code, encoding="utf-8")
        logger.info(f"💾 Writing runner code to: {runner_path}")
        runner_path.write_text(runner_code, encoding="utf-8")
        _safe_print(f"Generated logic file: {logic_path}")
        _safe_print(f"Generated runner file: {runner_path}")


# -----------------------------------------------------------------------------
# ⚙️ Data Processing & Configuration Helpers
# -----------------------------------------------------------------------------
# Functions for loading, parsing, and processing the machine data from JSON.
# -----------------------------------------------------------------------------


def _process_all_configurations(
    json_paths: List[str], parser: argparse.ArgumentParser
) -> Tuple[
    List[Dict[str, Any]],
    List[str],
    List[str],
    List[str],
    Set[str],
    Set[str],
    Set[str],
]:
    """Loads JSON configs, extracts names, and aggregates all logic components.

    This function iterates through each JSON path, loading and processing it
    to build comprehensive lists of configurations and their associated logic.

    Args:
        json_paths (List[str]): The list of file paths to load.
        parser (argparse.ArgumentParser): The parser instance for error reporting.

    Returns:
        Tuple containing:
        - List[Dict[str, Any]]: Raw configurations.
        - List[str]: Original machine IDs.
        - List[str]: snake_cased machine names.
        - List[str]: JSON base filenames.
        - Set[str]: All unique action names.
        - Set[str]: All unique guard names.
        - Set[str]: All unique service names.
    """
    logger.info("📜 Loading and parsing configuration files...")
    configs, machine_ids, machine_names, json_filenames = [], [], [], []
    all_actions, all_guards, all_services = set(), set(), set()

    for jp in json_paths:
        path = Path(jp)
        if not path.exists():
            logger.error(f"❌ JSON file not found: {jp}")
            parser.error(f"JSON file not found: {jp}")

        try:
            with open(path, "r", encoding="utf-8") as f:
                conf = json.load(f)

            # 🧬 Extract logic names and metadata from the config
            raw_name = conf.get("id", path.stem)
            actions, guards, services = extract_logic_names(conf)

            # ➕ Aggregate results
            configs.append(conf)
            machine_ids.append(raw_name)
            machine_names.append(camel_to_snake(raw_name))
            json_filenames.append(path.name)
            all_actions.update(actions)
            all_guards.update(guards)
            all_services.update(services)

        except json.JSONDecodeError as e:
            logger.error(f"❌ Invalid JSON in file {jp}: {e}")
            parser.error(f"Invalid JSON in file {jp}: {e}")
        except Exception as e:
            logger.error(f"❌ Could not process file {jp}: {e}")
            parser.error(f"Could not process file {jp}: {e}")

    logger.info("✅ Configurations loaded and initial logic extracted.")
    return (
        configs,
        machine_ids,
        machine_names,
        json_filenames,
        all_actions,
        all_guards,
        all_services,
    )


def _parse_boolean_flags(
    args: argparse.Namespace, parser: argparse.ArgumentParser
) -> Dict[str, bool]:
    """Parses all string boolean arguments into actual booleans.

    Args:
        args (argparse.Namespace): The parsed command-line arguments.
        parser (argparse.ArgumentParser): The parser instance for error reporting.

    Returns:
        Dict[str, bool]: A dictionary mapping flag names to their boolean values.
    """
    try:
        result = {
            "loader": normalize_bool(args.loader),
            "sleep": normalize_bool(args.sleep),
            "log": normalize_bool(args.log),
        }
        if args.async_mode is not None:
            result["async_mode"] = normalize_bool(args.async_mode)
        else:
            result["async_mode"] = True  # Default for backward compat
        return result
    except ValueError as e:
        parser.error(str(e))


# -----------------------------------------------------------------------------
# 🤝 Hierarchy & Interaction Helpers
# -----------------------------------------------------------------------------
# Functions dedicated to managing machine hierarchy, including interactive
# prompts for user validation.
# -----------------------------------------------------------------------------


def _reorder_lists_by_parent_index(
    data_lists: List[List[Any]], parent_index: int
) -> List[List[Any]]:
    """Reorders a collection of lists to place the parent element first.

    Args:
        data_lists (List[List[Any]]): A list of lists to be reordered in unison.
        parent_index (int): The index of the parent element.

    Returns:
        List[List[Any]]: The reordered lists.
    """
    if parent_index != 0:
        for lst in data_lists:
            lst.insert(0, lst.pop(parent_index))
    return data_lists


def _handle_interactive_hierarchy(
    data_tuple: Tuple[List, ...],
) -> Tuple[bool, Tuple[List, ...]]:
    """Manages the interactive prompt for heuristic parent detection.

    Args:
        data_tuple (Tuple[List, ...]): A tuple containing lists to be managed:
            (configs, machine_ids, machine_names, json_paths, json_filenames)

    Returns:
        A tuple containing:
        - bool: The final hierarchy flag.
        - Tuple[List, ...]: The input lists, potentially reordered.
    """
    configs, machine_ids, machine_names, json_paths, json_filenames = (
        data_tuple
    )
    path_to_id_map = dict(zip(json_paths, machine_ids))
    parent_path, _, scores = guess_hierarchy(json_paths)
    final_parent_path = None

    print(f"Found {len(machine_ids)} machines:")
    # FIX: Use the exact wording the test suite expects for the prompt.
    for i, (path, score) in enumerate(scores):
        display_id = path_to_id_map.get(path, Path(path).stem)
        if path == parent_path:
            print(
                f"  {i + 1}. {display_id} (looks like parent, score: {score})"
            )
        else:
            print(
                f"  {i + 1}. {display_id} (looks like child, score: {score})"
            )

    answer = input("Is this correct? [Y/n] ").strip().lower()

    if answer in ("", "y", "yes"):
        final_parent_path = parent_path
    else:
        print("\nWhich machine is the parent?")
        for i, name in enumerate(machine_ids):
            print(f"  {i + 1}. {name}")
        try:
            selection = int(input("Enter number (or 0 for none): ").strip())
            if 1 <= selection <= len(machine_ids):
                final_parent_path = json_paths[selection - 1]
        except (ValueError, IndexError):
            logger.warning(
                "⚠️ Invalid selection. Proceeding without hierarchy."
            )

    if final_parent_path:
        parent_index = json_paths.index(final_parent_path)
        logger.info(f"👑 Setting '{machine_ids[parent_index]}' as parent.")
        reordered_data = _reorder_lists_by_parent_index(
            list(data_tuple), parent_index
        )
        return True, tuple(reordered_data)

    return False, data_tuple


# -----------------------------------------------------------------------------
# 🧩 Code Generation & Merging Helpers
# -----------------------------------------------------------------------------
# Functions that assist in the final steps of code generation and file merging.
# -----------------------------------------------------------------------------


def _merge_code_for_single_file(logic_code: str, runner_code: str) -> str:
    """Intelligently merges logic and runner code into a single file string.

    It de-duplicates imports and logger configurations to create a clean,
    runnable single-file script.  Handles both single-line and multi-line
    (parenthesized) import statements.

    Args:
        logic_code (str): The generated logic code.
        runner_code (str): The generated runner code.

    Returns:
        str: The combined and cleaned code as a single string.
    """

    def _split_imports_and_body(
        lines: List[str],
    ) -> Tuple[List[str], List[str]]:
        """Split source lines into import lines and body lines.

        Multi-line imports like ``from x import (\\n  a,\\n  b,\\n)`` are
        collected as a *single* joined string so they can be de-duplicated
        as a unit.
        """
        import_strs: List[str] = []
        body: List[str] = []
        i = 0
        while i < len(lines):
            line = lines[i]
            if line.startswith("import ") or line.startswith("from "):
                # Check for multi-line import (opening paren without close)
                if "(" in line and ")" not in line:
                    # Gather continuation lines until closing paren
                    multi = [line]
                    i += 1
                    while i < len(lines):
                        multi.append(lines[i])
                        if ")" in lines[i]:
                            i += 1
                            break
                        i += 1
                    import_strs.append("\n".join(multi))
                else:
                    import_strs.append(line)
                    i += 1
            else:
                body.append(line)
                i += 1
        return import_strs, body

    logic_lines = logic_code.splitlines()
    runner_lines = runner_code.splitlines()

    # 🕵️‍♂️ Collect all unique top-level import blocks from both parts.
    logic_imports, logic_body = _split_imports_and_body(logic_lines)
    runner_imports, runner_body = _split_imports_and_body(runner_lines)

    # ---------------------------------------------------------------
    # Symbol-level dedup for ``from X import ...`` statements.
    # Plain ``import X`` statements are kept as-is (string dedup).
    # ---------------------------------------------------------------
    import re as _re

    # Bucket: module_path -> ordered list of symbols
    from_imports: dict[str, list[str]] = {}
    # Ordered list of ``import X`` (non-from) statements
    plain_imports: List[str] = []
    _seen_plain: Set[str] = set()

    def _parse_from_import(stmt: str) -> Tuple[str, List[str]]:
        """Extract (module, [symbols]) from a ``from M import ...``."""
        # Flatten multi-line into single line for parsing
        flat = " ".join(stmt.split())
        # Pattern: from <module> import (<symbols>) or from <module> import <symbols>
        m = _re.match(r"from\s+([\w.]+)\s+import\s+\(?(.*?)\)?\s*$", flat)
        if m:
            mod = m.group(1)
            symbols = [
                s.strip().rstrip(",")
                for s in m.group(2).split(",")
                if s.strip().rstrip(",")
            ]
            return mod, symbols
        return "", []

    for imp in logic_imports + runner_imports:
        if imp.startswith("from "):
            mod, symbols = _parse_from_import(imp)
            if mod:
                if mod not in from_imports:
                    from_imports[mod] = []
                for sym in symbols:
                    if sym not in from_imports[mod]:
                        from_imports[mod].append(sym)
            else:
                # Could not parse — keep raw
                if imp not in _seen_plain:
                    _seen_plain.add(imp)
                    plain_imports.append(imp)
        else:
            if imp not in _seen_plain:
                _seen_plain.add(imp)
                plain_imports.append(imp)

    # Reconstruct from-imports as single-line statements
    merged_from: List[str] = []
    for mod, symbols in from_imports.items():
        if len(symbols) == 1:
            merged_from.append(f"from {mod} import {symbols[0]}")
        else:
            merged_from.append(f"from {mod} import {', '.join(symbols)}")

    imports = sorted(plain_imports + merged_from)

    # 🏃 Strip logger setup from runner body to avoid duplicates —
    # but keep ``logging.basicConfig(...)`` if the logic body does NOT
    # already contain it.
    logic_has_basic_config = any(
        line.startswith("logging.basicConfig") for line in logic_body
    )
    runner_body = [
        line
        for line in runner_body
        if (
            not (
                line.startswith("logging.basicConfig")
                and logic_has_basic_config
            )
        )
        and "logger = logging.getLogger" not in line
    ]

    # 🧩 Assemble the final merged code
    # Find header from the logic file
    header_lines: List[str] = []
    for line in logic_lines:
        if line.strip().startswith("#") or not line.strip():
            header_lines.append(line)
        else:
            # Stop at the first actual code line
            break

    # Remove the header part from the logic body we extracted earlier
    logic_body_clean = logic_body[len(header_lines) :]

    final_lines = (
        header_lines
        + imports
        + [""]
        + logic_body_clean
        + ["", "# Runner part", ""]
        + runner_body
    )
    return "\n".join(final_lines)


# -----------------------------------------------------------------------------
# 🚀 Main Orchestrator
# -----------------------------------------------------------------------------
# The primary functions that drive the CLI execution flow.
# -----------------------------------------------------------------------------


def run_generation_workflow(
    args: argparse.Namespace, parser: argparse.ArgumentParser
) -> None:
    """Orchestrates the entire code generation workflow from start to finish.

    Args:
        args (argparse.Namespace): The parsed command-line arguments.
        parser (argparse.ArgumentParser): The CLI parser for error handling.
    """
    # 1. 📂 Process and validate input file paths
    json_paths = _get_validated_json_paths(args, parser)

    # 2. ⚙️ Load and process all machine configurations
    (
        configs,
        machine_ids,
        machine_names,
        json_filenames,
        all_actions,
        all_guards,
        all_services,
    ) = _process_all_configurations(json_paths, parser)

    # 3. 🤝 Determine hierarchy
    hierarchy_flag = bool(args.json_parent or args.json_child)
    if not hierarchy_flag and len(json_paths) > 1:
        data_tuple = (
            configs,
            machine_ids,
            machine_names,
            json_paths,
            json_filenames,
        )
        hierarchy_flag, updated_data = _handle_interactive_hierarchy(
            data_tuple
        )
        (
            configs,
            machine_ids,
            machine_names,
            json_paths,
            json_filenames,
        ) = updated_data

    # 4. 🗺️ Determine output paths
    paths = _determine_output_paths(
        args, machine_names, hierarchy_flag, json_paths
    )

    # 5. 🛡️ Check for existing files before proceeding
    files_to_check = (
        [paths["single_file"]]
        if args.file_count == 1
        else [paths["logic_file"], paths["runner_file"]]
    )
    if not args.force and any(f.exists() for f in files_to_check):
        # Prompt the user for confirmation to overwrite existing files.
        answer = (
            input("File(s) already exist. Overwrite? [Y/n] ").strip().lower()
        )
        # If the user enters 'n' or 'no', abort the operation.
        # The default is 'yes', so pressing Enter will proceed.
        if answer in ("n", "no"):
            print("Operation cancelled.")
            return

    # 6. 🔧 Parse boolean settings from args
    settings = _parse_boolean_flags(args, parser)

    # 7. 📜 Resolve template, async mode, and generate code via strategy
    logger.info("🤖 Generating logic and runner code...")
    try:
        template = resolve_template(
            getattr(args, "style", None),
            getattr(args, "template", None),
        )
    except ValueError as e:
        parser.error(str(e))

    is_async = resolve_async_mode(args.async_mode, template)

    # Build context and generate code
    strategy = get_strategy(template)
    machine_name = (
        machine_names[0] if hierarchy_flag else "_".join(machine_names)
    )
    machine_id = machine_ids[0]

    ctx = GenerationContext(
        actions=all_actions,
        guards=all_guards,
        services=all_services,
        is_async=is_async,
        log=settings["log"],
        machine_name=machine_name,
        machine_id=machine_id,
        machine_names=machine_names,
        machine_ids=machine_ids,
        file_count=args.file_count,
        configs=configs,
        json_filenames=json_filenames,
        hierarchy=hierarchy_flag,
        sleep=settings["sleep"],
        sleep_time=args.sleep_time,
        loader=settings["loader"],
        style=getattr(args, "style", None),
    )
    logic_code = strategy.generate_logic(ctx)
    runner_code = strategy.generate_runner(ctx)
    logger.info("✅ Code generation complete.")

    # 8. 💾 Write generated code to files
    _write_output_files(args.file_count, paths, logic_code, runner_code)


def run_list_templates() -> None:
    """Lists all available code generation templates with descriptions."""
    templates = [
        (
            "class-json",
            "Class + JSON",
            "OOP logic class with MachineLogic, bound to a JSON config loaded at runtime.",
        ),
        (
            "function-json",
            "Functions + JSON",
            "Module-level functions with LogicLoader auto-discovery, JSON config at runtime.",
        ),
        (
            "pythonic-class",
            "Class-Based",
            "StateMachine subclass with @action, @guard, @service decorators. Pure Python.",
        ),
        (
            "pythonic-builder",
            "Builder Pattern",
            "Fluent MachineBuilder API for dynamic, programmatic machine construction.",
        ),
        (
            "pythonic-functional",
            "Functional",
            "Simple build_machine() call with explicit state and transition definitions.",
        ),
    ]
    _safe_print("\nAvailable code generation templates:\n")
    _safe_print(f"  {'Template ID':<24} {'Style':<20} Description")
    _safe_print(f"  {'-' * 23}  {'-' * 19} {'-' * 55}")
    for tid, style, desc in templates:
        _safe_print(f"  {tid:<24} {style:<20} {desc}")
    _safe_print(
        "\nUsage: xsm generate-template <file.json> --template <template-id>\n"
    )


def run_validate(args: argparse.Namespace) -> None:
    """Validates one or more XState JSON config files."""
    errors = 0
    for jp in args.json_files:
        path = Path(jp)
        if not path.exists():
            _safe_print(f"  x {jp} -- file not found")
            errors += 1
            continue
        try:
            with open(path, "r", encoding="utf-8") as f:
                conf = json.load(f)
        except json.JSONDecodeError as e:
            _safe_print(f"  x {jp} -- invalid JSON: {e}")
            errors += 1
            continue

        # Validate structure
        issues = []
        actions, guards, services = set(), set(), set()
        if not isinstance(conf, dict):
            issues.append("root must be a JSON object")
        else:
            if "id" not in conf:
                issues.append("missing 'id' field")
            if "initial" not in conf and conf.get("type") != "parallel":
                issues.append("missing 'initial' field")
            if "states" not in conf:
                issues.append("missing 'states' field")
            elif not isinstance(conf.get("states"), dict):
                issues.append("'states' must be an object")
            else:
                if (
                    conf.get("initial")
                    and conf["initial"] not in conf["states"]
                ):
                    issues.append(
                        f"initial state '{conf['initial']}' not found in states"
                    )

            # Extract logic names for summary
            try:
                actions, guards, services = extract_logic_names(conf)
            except Exception:
                pass

        if issues:
            _safe_print(f"  x {jp} -- {len(issues)} issue(s):")
            for issue in issues:
                _safe_print(f"      - {issue}")
            errors += 1
        else:
            state_count = len(conf.get("states", {}))
            machine_id = conf.get("id", path.stem)
            _safe_print(f"  ok {jp}")
            _safe_print(f"      Machine: {machine_id}")
            _safe_print(f"      States:  {state_count}")
            if actions:
                _safe_print(f"      Actions: {', '.join(sorted(actions))}")
            if guards:
                _safe_print(f"      Guards:  {', '.join(sorted(guards))}")
            if services:
                _safe_print(f"      Services: {', '.join(sorted(services))}")

    if errors:
        _safe_print(f"\n{errors} file(s) had errors.")
        raise SystemExit(1)
    else:
        _safe_print(f"\nAll {len(args.json_files)} file(s) are valid.")


def run_info() -> None:
    """Displays library information and environment details."""
    import platform
    from .. import __version__ as pkg_ver

    _safe_print("\n  XState-StateMachine CLI")
    _safe_print("  ----------------------------------")
    _safe_print(f"  Version:      {pkg_ver}")
    _safe_print(f"  Python:       {platform.python_version()}")
    _safe_print(f"  Platform:     {platform.system()} {platform.machine()}")
    _safe_print(f"  Install path: {Path(__file__).resolve().parent.parent}")
    _safe_print("")
    _safe_print("  Features:")
    _safe_print("    * Async + Sync interpreters")
    _safe_print("    * XState JSON compatibility")
    _safe_print("    * Pythonic API (class, builder, functional)")
    _safe_print("    * Hierarchical & parallel states")
    _safe_print("    * Guards, actions, services, delayed transitions")
    _safe_print("    * Actor model (spawn child machines)")
    _safe_print("    * Plugin system & LoggingInspector")
    _safe_print("    * Snapshot save/restore")
    _safe_print("    * Diagram export (Mermaid, PlantUML, ASCII)")
    _safe_print("    * CLI code generator (5 templates)")
    _safe_print("    * Zero external dependencies")
    _safe_print("")
    _safe_print(
        "  Documentation: https://basiltt.github.io/xstate-statemachine/"
    )
    _safe_print(
        "  PyPI:          https://pypi.org/project/xstate-statemachine/"
    )
    _safe_print(
        "  GitHub:        https://github.com/basiltt/xstate-statemachine"
    )
    _safe_print("")


def main() -> None:
    """Parses CLI arguments and orchestrates the code generation workflow."""
    parser = get_parser()
    args = parser.parse_args()
    validate_args(
        parser
    )  # Note: Assuming this validates args based on the parser state.

    if args.subcommand in {"generate-template", "gt"}:
        run_generation_workflow(args, parser)
        return

    if args.subcommand in {"list-templates", "lt"}:
        run_list_templates()
        return

    if args.subcommand in {"validate", "val"}:
        run_validate(args)
        return

    if args.subcommand == "info":
        run_info()
        return

    # 🆘 Show help if no valid subcommand is given
    parser.print_help()


# -----------------------------------------------------------------------------
# 🎬 Entry Point
# -----------------------------------------------------------------------------
if __name__ == "__main__":
    main()
