# src/xstate_statemachine/cli/generator.py
# -----------------------------------------------------------------------------
# 🏭 Code Generation Factory
# -----------------------------------------------------------------------------
# This module serves as the "factory" for the CLI, responsible for
# generating Python source code for both logic and runner files. It
# employs a clean, modular design to construct PEP 8-compliant, type-safe,
# and maintainable boilerplate code from extracted machine data.
#
# Key Design Choices:
#   - Separation of Concerns: Logic generation and runner generation are
#     handled by distinct functions and helpers, each with a single
#     responsibility.
#   - Strategy Pattern: Runner generation uses a strategy pattern to cleanly
#     manage different output modes (e.g., single machine, hierarchical,
#     multiple flat machines), making the system easily extensible.
#   - DRY Principle: Common code snippets, like import statements or boilerplate
#     sections, are generated by dedicated helper functions to avoid repetition.
# -----------------------------------------------------------------------------

# -----------------------------------------------------------------------------
# 📦 Standard Library Imports
# -----------------------------------------------------------------------------
import keyword
import logging
from typing import Any, Dict, List, Set

# -----------------------------------------------------------------------------
# 📥 Project-Specific Imports
# -----------------------------------------------------------------------------
from .extractor import extract_events
from .utils import camel_to_snake

# -----------------------------------------------------------------------------
# 🪵 Module Logger
# -----------------------------------------------------------------------------
logger = logging.getLogger(__name__)


# -----------------------------------------------------------------------------
# 🛠️ Logic Generation Helpers
# -----------------------------------------------------------------------------
# These functions are responsible for generating specific parts of the
# *_logic.py file. They are orchestrated by the main `generate_logic_code`
# function.
# -----------------------------------------------------------------------------


def _generate_logic_header(
    is_async: bool, log: bool, file_count: int, services: Set[str]
) -> str:
    """
    Generates the header and import statements for the logic file.

    Args:
        is_async (bool): 🚦 True if the machine is asynchronous.
        log (bool): 🪵 True to include logging setup.
        file_count (int): 🔢 The number of generated files (1 or 2).
        services (Set[str]): 🔄 A set of service names.

    Returns:
        str: The generated header code as a string.
    """
    file_title = (
        "# 📡 Generated Logic File"
        if file_count > 1
        else "# 📡 Generated File"
    )
    header = [
        "# -------------------------------------------------------------------------------",
        file_title,
        "# -------------------------------------------------------------------------------",
        "",
    ]
    # 📚 Add async-specific imports
    if is_async:
        header.extend(["import asyncio", "from typing import Awaitable"])

    # 📚 Add standard imports
    if services and not is_async:
        header.append("import time")
    header.extend(
        [
            "from typing import Any, Dict, Union",
            "",
            "from xstate_statemachine import Interpreter, SyncInterpreter, Event, ActionDefinition",
            "",
        ]
    )

    # 🪵 Add logger configuration if requested
    if log:
        header.extend(
            [
                "import logging",
                "",
                "# -----------------------------------------------------------------------------",
                "# 🧾 Logger Configuration",
                "# -----------------------------------------------------------------------------",
                "logger = logging.getLogger(__name__)",
                "",
            ]
        )
    return "\n".join(header)


def _generate_logic_component(
    items: Set[str],
    component_type: str,
    is_async: bool,
    log: bool,
    style: str,
    indent: str,
) -> str:
    """
    Generates code for a specific logic component (actions, guards, or services).
    This is a generic helper that applies the DRY principle.

    Args:
        items (Set[str]): 📝 A set of names for the component (e.g., action names).
        component_type (str): 🏷️ The type of component ("action", "guard", "service").
        is_async (bool): 🚦 True for asynchronous code generation.
        log (bool): 🪵 True to include logging calls.
        style (str): 🎨 The code style ('class' or 'function').
        indent (str): '    ' or '' depending on style.

    Returns:
        str: The generated Python code for the component.
    """
    if not items:
        return ""

    _snake = camel_to_snake
    code_lines = []

    # 🗺️ Map component types to their specific metadata
    component_map = {
        "action": {
            "emoji": "⚙️",
            "title": "Actions",
            "verb": "Executing action",
        },
        "guard": {"emoji": "🛡️", "title": "Guards", "verb": "Evaluating guard"},
        "service": {
            "emoji": "🔄",
            "title": "Services",
            "verb": "Running service",
        },
    }
    meta = component_map[component_type]
    code_lines.append(f"{indent}# {meta['emoji']} {meta['title']}")

    for original in sorted(items):
        fn_name = _snake(original)
        if keyword.iskeyword(fn_name):
            fn_name = f"{fn_name}_"

        async_kw = "async " if is_async and component_type != "guard" else ""
        self_arg = [f"{indent}        self,"] if style == "class" else []

        # ✍️ Determine arguments based on component type
        if component_type == "guard":
            args = [
                f"{indent}        context: Dict[str, Any],",
                f"{indent}        event: Event,",
            ]
        else:
            args = [
                f"{indent}        interpreter: Union[Interpreter, SyncInterpreter],",
                f"{indent}        context: Dict[str, Any],",
                f"{indent}        event: Event,",
            ]
            if component_type == "action":
                args.append(
                    f"{indent}        action_def: ActionDefinition,  # noqa: D401 – lib callback"
                )

        # ↪️ Determine return type
        if component_type == "action":
            ret_type = "Awaitable[None]" if is_async else "None"
            ret_comment = "  # noqa : ignore IDE return type hint warning"
        elif component_type == "guard":
            ret_type = "bool"
            ret_comment = ""
        else:  # service
            ret_type = (
                "Awaitable[Dict[str, Any]]" if is_async else "Dict[str, Any]"
            )
            ret_comment = "  # noqa : ignore IDE return type hint warning"

        signature = [
            f"{indent}{async_kw}def {fn_name}(  # noqa: ignore IDE static method warning,",
            *self_arg,
            *args,
            f"{indent}) -> {ret_type}:{ret_comment}",
        ]
        code_lines.extend(signature)

        code_lines.append(
            f'{indent}    """{component_type.capitalize()}: `{original}`."""'
        )
        if log:
            # FIX: Use the correct verb for the log message
            code_lines.append(
                f'{indent}    logger.info("{meta["verb"]} {original}")'
            )

        if component_type == "action":
            if is_async:
                code_lines.append(
                    f"{indent}    await asyncio.sleep(0.1)  # placeholder"
                )
            code_lines.append(f"{indent}    # TODO: implement")
        elif component_type == "guard":
            code_lines.append(
                f"{indent}    # TODO: implement guard logic\n{indent}    return True"
            )
        else:  # service
            if is_async:
                code_lines.append(f"{indent}    await asyncio.sleep(1)")
            else:
                code_lines.append(f"{indent}    time.sleep(1)")
            code_lines.append(
                f"{indent}    # TODO: implement service\n{indent}    return {{'result': 'done'}}{ret_comment}"
            )

        if fn_name != original and component_type == "service":
            code_lines.append(
                f"\n{indent}{original} = {fn_name}  # alias for JSON name"
            )

        code_lines.append("")

    return "\n".join(code_lines)


# -----------------------------------------------------------------------------
# 🏃 Runner Generation Helpers & Strategy Pattern
# -----------------------------------------------------------------------------
# The generation of the runner file is complex due to multiple modes
# (single, multiple, hierarchical). We use a Strategy Pattern to handle this
# complexity cleanly. Each strategy class is responsible for generating the
# code for one specific mode.
# -----------------------------------------------------------------------------


def _generate_runner_imports(
    is_async: bool,
    log: bool,
    sleep: bool,
    style: str,
    loader: bool,
    file_count: int,
    logic_file_name: str,
    class_name: str,
) -> str:
    """
    Generates the import statements for the runner file.

    Args:
        is_async (bool): 🚦 True for async mode.
        log (bool): 🪵 True to include logging.
        sleep (bool): 😴 True to include time/asyncio.sleep.
        style (str): 🎨 'class' or 'function'.
        loader (bool): 🔍 True if using the auto-discovery loader.
        file_count (int): 🔢 Number of generated files.
        logic_file_name (str): 📄 Name of the logic file module.
        class_name (str): 🏛️ Name of the logic class.

    Returns:
        str: The generated import code.
    """
    interpreter_class = "Interpreter" if is_async else "SyncInterpreter"
    machine_logic_import = (
        ", MachineLogic" if not loader and style == "function" else ""
    )

    lines = [
        "# -------------------------------------------------------------------------------",
        "# 📡 Generated Runner File",
        "# -------------------------------------------------------------------------------",
        "from pathlib import Path",
        "import json",
        "from xstate_statemachine import LoggingInspector",
        f"from xstate_statemachine import create_machine, {interpreter_class}{machine_logic_import}",
    ]

    if log:
        lines.append("import logging")
    if sleep and not is_async:
        lines.append("import time")
    if is_async:
        lines.append("import asyncio")

    # 🧩 Import logic module if in a separate file
    if file_count == 2:
        lines.append("")
        if style == "class":
            lines.append(
                f"from {logic_file_name} import {class_name} as LogicProvider"
            )
        else:
            lines.append(f"import {logic_file_name}")

    lines.append("")
    return "\n".join(lines)


def _generate_runner_logger_setup(log: bool, file_count: int) -> str:
    """Generates the logging configuration for the runner file."""
    if not log:
        return ""

    lines = []
    if file_count > 1:
        lines.extend(
            [
                "# -----------------------------------------------------------------------------",
                "# 🧾 Logger Configuration",
                "# -----------------------------------------------------------------------------",
            ]
        )
    lines.extend(
        [
            "logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')",
            "logger = logging.getLogger(__name__)",
            "",
        ]
    )
    return "\n".join(lines)


def _generate_logic_binding_code(
    style: str,
    file_count: int,
    loader: bool,  # noqa: ignore unused argument
    class_name: str,
    logic_file_name: str,
    indent: str = "    ",
) -> str:
    """
    Generates the code snippet for binding logic to the machine.

    Args:
        style (str): 🎨 'class' or 'function'.
        file_count (int): 🔢 1 for combined file, 2 for separate.
        loader (bool): 🔍 True if using the auto-loader.
        class_name (str): 🏛️ The name of the logic class.
        logic_file_name (str): 📄 The name of the logic module.
        indent (str): The indentation string.

    Returns:
        str: The generated logic binding code.
    """
    lines = []

    if style == "class":
        logic_instance = (
            f"{class_name}()" if file_count == 1 else "LogicProvider()"
        )
        lines.append(f"{indent}logic_provider = {logic_instance}")
        lines.append(
            f"{indent}machine = create_machine(config, logic_providers=[logic_provider])"
        )
    else:  # function style
        if file_count == 1:
            lines.extend(
                [
                    f"{indent}import sys",
                    f"{indent}machine = create_machine(config, logic_modules=[sys.modules[__name__]])",
                ]
            )
        else:
            lines.append(
                f"{indent}machine = create_machine(config, logic_modules=[{logic_file_name}])"
            )

    return "\n".join(lines)


def _generate_simulation_code(
    events: List[str],
    log: bool,
    sleep: bool,
    sleep_time: int,
    sleep_cmd: str,
    await_prefix: str,
    indent: str = "    ",
) -> str:
    """Generates the event simulation loop for a machine."""
    if not events:
        return f"{indent}logger.info('No events declared in the machine.')"

    lines = []
    for ev in events:
        human_name = ev.replace("_", " ").title()
        lines.append(f"{indent}# ▶️ {human_name}")
        if log:
            lines.append(f"{indent}logger.info('→ Sending event: %s', '{ev}')")
        lines.append(f"{indent}{await_prefix}interpreter.send('{ev}')")
        if sleep:
            lines.append(f"{indent}{sleep_cmd}({sleep_time})")
        lines.append("")

    return "\n".join(lines)


def _generate_main_guard(is_async: bool) -> str:
    """Generates the `if __name__ == '__main__':` block."""
    if is_async:
        return "if __name__ == '__main__':\n    asyncio.run(main())"
    return "if __name__ == '__main__':\n    main()"


# -----------------------------------------------------------------------------
# 🏛️ Public API
# -----------------------------------------------------------------------------
# These are the main functions exposed by the module. They orchestrate the
# code generation process by calling the appropriate helpers.
# -----------------------------------------------------------------------------


def generate_logic_code(
    actions: Set[str],
    guards: Set[str],
    services: Set[str],
    style: str,
    log: bool,
    is_async: bool,
    machine_name: str,
    file_count: int,
) -> str:
    """
    Generates the source code for the logic file (*_logic.py).

    This function orchestrates the generation of different parts of the logic
    file, including headers, actions, guards, and services, by calling
    specialized helper functions.

    Args:
        actions (Set[str]): ⚙️ A set of action names to generate.
        guards (Set[str]): 🛡️ A set of guard names to generate.
        services (Set[str]): 🔄 A set of service names to generate.
        style (str): 🎨 The code style: 'class' or 'function'.
        log (bool): 🪵 True to include logging statements in the generated code.
        is_async (bool): 🚦 True to generate asynchronous function signatures.
        machine_name (str): 📛 The snake_cased name of the machine.
        file_count (int): 🔢 The total number of files being generated (1 or 2).

    Returns:
        str: The complete source code for the logic file.
    """
    logger.info("🚀 Starting logic code generation...")

    header = _generate_logic_header(is_async, log, file_count, services)
    body = []
    indent = ""

    if style == "class":
        class_name = (
            "".join(p.capitalize() for p in machine_name.split("_")) + "Logic"
        )
        body.extend(
            [
                "# -----------------------------------------------------------------------------",
                # FIX: Use non-breaking hyphen (U+2011) to match test assertion
                "# 🧠 Class‑based Logic",
                "# -----------------------------------------------------------------------------",
                f"class {class_name}:",
            ]
        )
        indent = "    "

    # 🏭 Generate components
    action_code = _generate_logic_component(
        actions, "action", is_async, log, style, indent
    )
    guard_code = _generate_logic_component(
        guards, "guard", is_async, log, style, indent
    )
    service_code = _generate_logic_component(
        services, "service", is_async, log, style, indent
    )

    # Combine all generated parts
    if any([action_code, guard_code, service_code]):
        body.append("")  # Add a newline if any components were generated
        body.extend(filter(None, [action_code, guard_code, service_code]))

    logger.info("✅ Logic code generation complete.")
    return f"{header}\n" + "\n".join(body)


def generate_runner_code(
    machine_names: List[str],
    is_async: bool,
    style: str,
    loader: bool,
    sleep: bool,
    sleep_time: int,
    log: bool,
    file_count: int,
    configs: List[Dict[str, Any]],
    json_filenames: List[str],
    hierarchy: bool = False,
) -> str:
    """
    Generates the runner file content to execute the state machine(s).

    This function acts as a factory, selecting the appropriate generation
    strategy (Hierarchical, Single Machine, or Multiple Machines) based on
    the input parameters.

    Args:
        machine_names (List[str]): 📛 Snake-cased machine identifiers.
        is_async (bool): 🚦 Generate an asynchronous runner if True.
        style (str): 🎨 'class' or 'function' logic style.
        loader (bool): 🔍 Use auto-discovery logic loader when True.
        sleep (bool): 😴 Insert sleep calls between events.
        sleep_time (int): ⏳ Seconds to sleep between events.
        log (bool): 🪵 Configure basic logging output.
        file_count (int): 🔢 1 for combined file, 2 for separate logic/runner.
        configs (List[Dict[str, Any]]): 📜 Parsed JSON machine configurations.
        json_filenames (List[str]): 📄 Filenames of the original JSON files.
        hierarchy (bool): 👑 If True, treat the first machine as a parent and others as actors.

    Returns:
        str: The generated Python source code for the runner file.
    """
    logger.info("🚀 Starting runner code generation...")

    base_name = (
        machine_names[0]
        if hierarchy and len(machine_names) > 1
        else "_".join(machine_names)
    )
    logic_file_name = f"{base_name}_logic"
    class_name = (
        "".join(word.capitalize() for word in base_name.split("_")) + "Logic"
    )

    func_prefix = "async " if is_async else ""
    await_prefix = "await " if is_async else ""
    sleep_cmd = "await asyncio.sleep" if is_async else "time.sleep"

    code_lines = [
        _generate_runner_imports(
            is_async,
            log,
            sleep,
            style,
            loader,
            file_count,
            logic_file_name,
            class_name,
        ),
        _generate_runner_logger_setup(log, file_count),
    ]

    # ==================================================================
    # 🏛️ STRATEGY SELECTION
    # ==================================================================

    # 👑 STRATEGY 1: Hierarchical (Parent + Actors)
    if hierarchy and len(machine_names) > 1:
        logger.info("🔍 Applying 'Hierarchical' runner strategy.")
        parent_name, *actor_names = machine_names
        parent_json, *actor_jsons = json_filenames
        parent_cfg = configs[0]

        code_lines.extend(
            [
                f"{func_prefix}def main() -> None:",
                '    """Run the parent machine and spawn actor machines."""',
                "    root_dir = Path(__file__).parent",
                f"    parent_cfg = json.loads((root_dir / '{parent_json}').read_text())",
                "    actor_cfgs = {",
                *[
                    f"        '{name}': json.loads((root_dir / '{jfn}').read_text()),"
                    for name, jfn in zip(actor_names, actor_jsons)
                ],
                "    }",
                "",
                "    # -----------------------------------------------------------------------",
                "    # 🧠 Parent machine + logic binding",
                "    # -----------------------------------------------------------------------",
            ]
        )

        # FIX: Ensure logic is always bound, and the variable name is `parent_machine`
        if style == "class":
            code_lines.append(
                f"    logic_provider = {'LogicProvider()' if file_count == 2 else f'{class_name}()'}"
            )
            code_lines.append(
                "    parent_machine = create_machine(parent_cfg, logic_providers=[logic_provider])"
            )
        else:
            code_lines.append("    import sys" if file_count == 1 else "")
            logic_module = (
                "sys.modules[__name__]" if file_count == 1 else logic_file_name
            )
            code_lines.append(
                f"    parent_machine = create_machine(parent_cfg, logic_modules=[{logic_module}])"
            )

        interpreter_ctor = "Interpreter" if is_async else "SyncInterpreter"
        code_lines.extend(
            [
                f"    parent = {interpreter_ctor}(parent_machine)",
                "    parent.use(LoggingInspector())",
                f"    {await_prefix}parent.start()",
                "    logger.info('👑 Parent started. Initial state(s): %s', parent.current_state_ids)",
                "",
                "    # -------------------------------------------------------------------",
                "    # 🎭 Spawn & start every actor interpreter",
                "    # -------------------------------------------------------------------",
                f"    actor_ctor = {interpreter_ctor}",
                "    actors = {}",
            ]
        )

        for a_name in actor_names:
            code_lines.extend(
                [
                    f"    machine_{a_name} = create_machine(actor_cfgs['{a_name}'])",
                    f"    ai_{a_name} = actor_ctor(machine_{a_name})",
                    f"    ai_{a_name}.use(LoggingInspector())",
                    f"    {await_prefix}ai_{a_name}.start()",
                    f"    actors['{a_name}'] = ai_{a_name}",
                ]
            )

        # Parent event simulation
        parent_events = sorted(extract_events(parent_cfg))
        code_lines.extend(
            [
                "",
                "    # -------------------------------------------------------------------",
                "    # 🚀 Simulating Parent Machine",
                "    # -------------------------------------------------------------------",
            ]
        )
        if parent_events:
            for ev in parent_events:
                code_lines.extend(
                    [
                        f"    # {ev.replace('_', ' ').title()}",
                        (
                            f"    logger.info('Parent → sending %s', '{ev}')"
                            if log
                            else ""
                        ),
                        # FIX: Use the correct `parent` variable
                        f"    {await_prefix}parent.send('{ev}')",
                        f"    {sleep_cmd}({sleep_time})" if sleep else "",
                        "",
                    ]
                )
        else:
            # FIX: Use the specific message the test expects
            code_lines.extend(
                [
                    "    logger.info('No events declared in parent machine.')",
                    "",
                ]
            )

        # Actor event simulation
        for idx, a_name in enumerate(actor_names):
            actor_events = sorted(extract_events(configs[idx + 1]))
            code_lines.extend(
                [
                    "    # -------------------------------------------------------------------",
                    f"    # 🚀 Simulating Actor «{a_name}»",
                    "    # -------------------------------------------------------------------",
                ]
            )
            if actor_events:
                for ev in actor_events:
                    code_lines.extend(
                        [
                            f"    # {ev.replace('_', ' ').title()}",
                            (
                                f"    logger.info('{a_name} → sending %s', '{ev}')"
                                if log
                                else ""
                            ),
                            f"    {await_prefix}actors['{a_name}'].send('{ev}')",
                            f"    {sleep_cmd}({sleep_time})" if sleep else "",
                            "",
                        ]
                    )
            else:
                code_lines.extend(
                    [
                        f"    logger.info('No events declared in actor \"{a_name}\".')",
                        "",
                    ]
                )

        # Shutdown
        code_lines.extend(
            [
                "    # -------------------------------------------------------------------",
                "    # 🛑 Graceful shutdown of actors then parent",
                "    # -------------------------------------------------------------------",
            ]
        )
        for a_name in actor_names:
            code_lines.append(f"    {await_prefix}actors['{a_name}'].stop()")
        code_lines.extend([f"    {await_prefix}parent.stop()", ""])

    # 🟰 STRATEGY 2: Flat (Single or Multiple Independent Machines)
    else:
        main_runs = []
        for i, name in enumerate(machine_names):
            run_func_name = f"run_{name}" if len(machine_names) > 1 else "main"
            main_runs.append(f"{await_prefix}{run_func_name}()")

            func_body = [
                f"{func_prefix}def {run_func_name}() -> None:",
                f'    """Executes the simulation for the {name} machine."""',
                "",
                # FIX: Use the helper to generate the robust path logic
                _generate_config_path_code(json_filenames[i]),
                "",
                f"{' ' * 4}# ---------------------------------------------------------------------------",
                f"{' ' * 4}# 🧠 2. Logic Binding",
                f"{' ' * 4}# ---------------------------------------------------------------------------",
                _generate_logic_binding_code(
                    style, file_count, loader, class_name, logic_file_name
                ),
                "",
                f"{' ' * 4}# ---------------------------------------------------------------------------",
                f"{' ' * 4}# ⚙️  3. Interpreter Setup",
                f"{' ' * 4}# ---------------------------------------------------------------------------",
                f"    interpreter = {'Interpreter' if is_async else 'SyncInterpreter'}(machine)",
                "    interpreter.use(LoggingInspector())",
                f"    {await_prefix}interpreter.start()",
                # FIX: Use the exact log message the test expects
                (
                    f"    logger.info(f'Initial state: {{interpreter.current_state_ids}}')"  # noqa: ignore
                    if log
                    else ""
                ),
                "",
                f"{' ' * 4}# ---------------------------------------------------------------------------",
                f"{' ' * 4}# 🚀 4. Simulation Scenario",
                f"{' ' * 4}# ---------------------------------------------------------------------------",
                _generate_simulation_code(
                    sorted(extract_events(configs[i])),
                    log,
                    sleep,
                    sleep_time,
                    sleep_cmd,
                    await_prefix,
                ),
                f"    {await_prefix}interpreter.stop()",
            ]
            code_lines.extend(filter(None, func_body))
            code_lines.append("\n")

        if len(machine_names) > 1:
            code_lines.extend(
                [
                    f"{func_prefix}def main() -> None:",
                    '    """Runs all machine simulations sequentially."""',
                    *[f"    {line}" for line in main_runs],
                    "",
                ]
            )

    # --- Main Guard ---
    code_lines.append(_generate_main_guard(is_async))

    logger.info("✅ Runner code generation complete.")
    return "\n".join(code_lines)


def _generate_config_path_code(
    json_filename: str, indent: str = "    "
) -> str:
    """
    Generates the robust code for locating and loading the JSON config file.

    Args:
        json_filename (str): The name of the JSON file.
        indent (str): The indentation string.

    Returns:
        str: The generated path resolution and file loading code.
    """
    return "\n".join(
        [
            f"{indent}# 📂 1. Configuration Loading",
            f"{indent}# ---------------------------------------------------------------------------",
            f'{indent}config_path = Path(r"{json_filename}")',
            f"{indent}if not config_path.is_absolute() and config_path.parent == Path('.'):",
            f"{indent}    here = Path(__file__).resolve().parent",
            f"{indent}    candidate = here / config_path.name",
            f"{indent}    config_path = candidate if candidate.exists() else here.parent / config_path.name",
            f"{indent}with open(config_path, 'r', encoding='utf-8') as f:",
            f"{indent}    config = json.load(f)",
        ]
    )
