"""
State Machine Code Generator — SMCG (Spec 62)

Reads a state machine definition and generates Python source code
that uses the runtime engine (Spec 61).
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import Optional

from smcraft.model import (
    ActionDef,
    StateDef,
    StateKindType,
    TransitionDef,
)
from smcraft.parser import EnrichedModel, StateMachineParser


def to_snake_case(name: str) -> str:
    """Convert PascalCase/camelCase to snake_case."""
    s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
    s = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", s)
    return s.lower()


def to_class_name(name: str) -> str:
    """Ensure a valid Python class name (PascalCase)."""
    return name.replace("_", "").replace("-", "").replace(" ", "")


def python_type(type_str: str) -> str:
    """Map definition types to Python types."""
    mapping = {
        "string": "str",
        "int": "int",
        "float": "float",
        "double": "float",
        "bool": "bool",
        "boolean": "bool",
        "object": "Any",
        "Object": "Any",
    }
    return mapping.get(type_str, type_str)


class PythonCodeGenerator:
    """Generates Python state machine code from an enriched model."""

    def __init__(self, model: EnrichedModel):
        self.model = model
        self.defn = model.definition
        self.settings = self.defn.settings
        self.lines: list[str] = []
        self._indent = 0

    def generate(self) -> str:
        """Generate complete Python source code."""
        self.lines = []
        self._emit_header()
        self._emit_imports()
        self._emit_blank()
        self._emit_state_enum()
        self._emit_blank()
        self._emit_base_state_class()
        self._emit_blank()
        self._emit_leaf_state_classes()
        self._emit_context_class()
        self._emit_blank()
        self._emit_feeder_classes()
        return "\n".join(self.lines)

    # --- Emitters ---

    def _emit_header(self) -> None:
        name = self.settings.name or "StateMachine"
        self._line(f'"""')
        self._line(f"Generated State Machine: {name}")
        self._line(f"Namespace: {self.settings.namespace}")
        self._line(f"")
        self._line(f"Generated by SMCG. Do not edit manually.")
        self._line(f'"""')
        self._emit_blank()

    def _emit_imports(self) -> None:
        self._line("from __future__ import annotations")
        self._emit_blank()
        self._line("from enum import IntEnum")
        self._line("from typing import Any, Optional")
        self._emit_blank()
        self._line("from smcraft.runtime import (")
        self._line("    Context,")
        self._line("    ContextAsync,")
        self._line("    ContextBase,")
        self._line("    ObserverNull,")
        self._line("    State,")
        self._line("    StateKind,")
        self._line("    TransitionHelper,")
        self._line(")")

    def _emit_state_enum(self) -> None:
        name = self.settings.name or "StateMachine"
        self._line(f"class {name}StateEnum(IntEnum):")
        self._indent += 1
        self._line(f'"""State enumeration for {name}."""')
        for i, state in enumerate(self.model.all_states):
            if not state.is_history:
                self._line(f"{to_snake_case(state.name).upper()} = {i}")
        self._indent -= 1

    def _emit_base_state_class(self) -> None:
        name = self.settings.name or "StateMachine"
        self._line(f"class State{name}(State):")
        self._indent += 1
        self._line(f'"""Base state with virtual event handlers for {name}."""')
        self._emit_blank()

        # Generate a virtual method for each event
        for evt_id, evt in self.model.event_map.items():
            params = ["self", "context"]
            for p in evt.parameters:
                params.append(f"{to_snake_case(p.name)}: {python_type(p.type)}")
            param_str = ", ".join(params)
            method_name = f"on_{to_snake_case(evt_id)}"
            self._line(f"def {method_name}({param_str}) -> None:")
            self._indent += 1
            self._line("pass")
            self._indent -= 1
            self._emit_blank()

        self._indent -= 1

    def _emit_leaf_state_classes(self) -> None:
        name = self.settings.name or "StateMachine"
        for state in self.model.all_states:
            if state.is_history:
                continue
            if not state.is_leaf:
                continue
            self._emit_state_class(state, name)
            self._emit_blank()

    def _emit_state_class(self, state: StateDef, machine_name: str) -> None:
        class_name = f"State{to_class_name(state.name)}"
        base_class = f"State{machine_name}"
        kind = "StateKind.FINAL" if state.is_final else "StateKind.LEAF"

        self._line(f"class {class_name}({base_class}):")
        self._indent += 1
        self._line(f'"""State: {state.name}"""')
        self._emit_blank()

        # Constructor
        self._line(f"def __init__(self, parent: Optional[State] = None):")
        self._indent += 1
        self._line(f'super().__init__(name="{state.name}", kind={kind}, parent=parent)')
        self._indent -= 1
        self._emit_blank()

        # on_entry
        if state.on_entry:
            self._line(f"def on_entry(self, context: ContextBase) -> None:")
            self._indent += 1
            for action in state.on_entry:
                self._emit_action(action, "context")
            self._indent -= 1
            self._emit_blank()

        # on_exit
        if state.on_exit:
            self._line(f"def on_exit(self, context: ContextBase) -> None:")
            self._indent += 1
            for action in state.on_exit:
                self._emit_action(action, "context")
            self._indent -= 1
            self._emit_blank()

        # Group transitions by event to avoid duplicate methods
        from collections import defaultdict
        transitions_by_event: dict[str, list[TransitionDef]] = defaultdict(list)
        for t in state.transitions:
            transitions_by_event[t.event].append(t)

        for event_id, transitions in transitions_by_event.items():
            self._emit_event_handler(state, event_id, transitions, machine_name)

        self._indent -= 1

    def _emit_event_handler(self, state: StateDef, event_id: str, transitions: list[TransitionDef], machine_name: str) -> None:
        evt = self.model.event_map.get(event_id)
        if not evt:
            return

        method_name = f"on_{to_snake_case(event_id)}"
        params = ["self", "context"]
        for p in evt.parameters:
            params.append(f"{to_snake_case(p.name)}: {python_type(p.type)}")
        param_str = ", ".join(params)

        self._line(f"def {method_name}({param_str}) -> None:")
        self._indent += 1

        for i, transition in enumerate(transitions):
            if transition.condition:
                keyword = "if" if i == 0 else "elif"
                self._line(f"{keyword} {transition.condition}:")
                self._indent += 1
                self._emit_transition_body(transition)
                self._indent -= 1
            elif len(transitions) > 1 and i > 0:
                # Unconditional fallback after conditional transitions
                self._line(f"else:")
                self._indent += 1
                self._emit_transition_body(transition)
                self._indent -= 1
            else:
                self._emit_transition_body(transition)

        self._indent -= 1
        self._emit_blank()

    def _emit_transition_body(self, transition: TransitionDef) -> None:
        if transition.next_state:
            target_attr = f"state_{to_snake_case(transition.next_state)}"
            trans_name = transition.event
            if transition.condition:
                trans_name = f"{transition.event}[{transition.condition}]"

            self._line(f"TransitionHelper.process_transition_begin(")
            self._line(f'    context, self, context.{target_attr}, "{trans_name}"')
            self._line(f")")

            for action in transition.actions:
                self._emit_action(action, "context")

            self._line(f"context.state_current = context.{target_attr}")
            self._line(f"TransitionHelper.process_transition_end(")
            self._line(f"    context, self, context.{target_attr}")
            self._line(f")")
        else:
            for action in transition.actions:
                self._emit_action(action, "context")

    def _emit_context_class(self) -> None:
        name = self.settings.name or "StateMachine"
        base = "ContextAsync" if self.settings.asynchronous else "Context"
        ctx_name = name + "Context"
        if self.settings.context and self.settings.context.cls:
            ctx_name = self.settings.context.cls

        self._line(f"class {ctx_name}({base}):")
        self._indent += 1
        self._line(f'"""Context for {name} state machine."""')
        self._emit_blank()

        # Constructor
        obj_params = ", ".join(
            [f"{o.instance}: {python_type(o.cls)}" for o in self.settings.objects]
        )
        init_params = "self"
        if obj_params:
            init_params += f", {obj_params}"

        self._line(f"def __init__({init_params}):")
        self._indent += 1
        if self.settings.asynchronous:
            self._line(f'super().__init__(name="{ctx_name}")')
        else:
            self._line(f'super().__init__(name="{ctx_name}")')

        # Store object references
        for obj in self.settings.objects:
            self._line(f"self.{obj.instance} = {obj.instance}")

        self._emit_blank()
        self._line("# Initialize states")

        # Build parent state instances for non-leaf states
        non_leaf_states = [s for s in self.model.all_states if not s.is_leaf and not s.is_history]
        for state in non_leaf_states:
            attr = f"state_{to_snake_case(state.name)}"
            kind = "StateKind.ROOT" if self.model.parent_map.get(state.name) is None else "StateKind.COMPOSITE"
            parent_name = self.model.parent_map.get(state.name)
            parent_ref = f"self.state_{to_snake_case(parent_name)}" if parent_name else "None"
            self._line(f'self.{attr} = State(name="{state.name}", kind={kind}, parent={parent_ref})')

        # Leaf state instances
        for state in self.model.all_states:
            if state.is_history or not state.is_leaf:
                continue
            attr = f"state_{to_snake_case(state.name)}"
            class_name = f"State{to_class_name(state.name)}"
            parent_name = self.model.parent_map.get(state.name)
            parent_ref = f"self.state_{to_snake_case(parent_name)}" if parent_name else "None"
            self._line(f"self.{attr} = {class_name}(parent={parent_ref})")

        self._indent -= 1
        self._emit_blank()

        # enter_initial_state
        if self.defn.root_state:
            initial = self._find_initial_leaf(self.defn.root_state)
            if initial:
                self._line(f"def enter_initial_state(self) -> None:")
                self._indent += 1
                self._line(f"self.state_current = self.state_{to_snake_case(initial.name)}")
                # Walk entry chain
                entry_chain = self._build_entry_chain(initial)
                for s in entry_chain:
                    self._line(f'self.observer.on_entry(self.name, "{s.name}")')
                    self._line(f"self.state_{to_snake_case(s.name)}.on_entry(self)")
                self._indent -= 1
                self._emit_blank()

        # set_state for deserialization
        self._line(f"def set_state(self, state_name: str) -> None:")
        self._indent += 1
        self._line(f"state_map = {{")
        self._indent += 1
        for state in self.model.all_states:
            if state.is_history:
                continue
            if state.is_leaf:
                attr = f"state_{to_snake_case(state.name)}"
                self._line(f'"{state.name}": self.{attr},')
        self._indent -= 1
        self._line(f"}}")
        self._line(f"if state_name in state_map:")
        self._indent += 1
        self._line(f"self.state_current = state_map[state_name]")
        self._indent -= 1
        self._indent -= 1
        self._emit_blank()

        # Event handler methods on context (delegate to current state)
        for evt_id, evt in self.model.event_map.items():
            params = ["self"]
            call_args = []
            for p in evt.parameters:
                snake = to_snake_case(p.name)
                params.append(f"{snake}: {python_type(p.type)}")
                call_args.append(snake)

            method_name = f"on_{to_snake_case(evt_id)}"
            param_str = ", ".join(params)
            args_str = ", ".join(["self"] + call_args)

            self._line(f"def {method_name}({param_str}) -> None:")
            self._indent += 1
            self._line(f"if self.state_current:")
            self._indent += 1
            self._line(f"self.state_current.{method_name}({args_str})")
            self._indent -= 1
            self._indent -= 1
            self._emit_blank()

        self._indent -= 1

    def _emit_feeder_classes(self) -> None:
        for feeder_name, events in self.model.feeders_map.items():
            self._emit_feeder_class(feeder_name, events)
            self._emit_blank()

    def _emit_feeder_class(self, feeder_name: str, events: list) -> None:
        name = self.settings.name or "StateMachine"
        ctx_name = name + "Context"
        if self.settings.context and self.settings.context.cls:
            ctx_name = self.settings.context.cls

        self._line(f"class {feeder_name}:")
        self._indent += 1
        self._line(f'"""Event feeder for {name}. Provides typed event methods."""')
        self._emit_blank()

        self._line(f"def __init__(self, context: {ctx_name}):")
        self._indent += 1
        self._line(f"self.context = context")
        self._indent -= 1
        self._emit_blank()

        for evt in events:
            params = ["self"]
            call_args = []
            for p in evt.parameters:
                snake = to_snake_case(p.name)
                params.append(f"{snake}: {python_type(p.type)}")
                call_args.append(snake)

            method_name = to_snake_case(evt.id)
            param_str = ", ".join(params)
            args_str = ", ".join(call_args)

            self._line(f"def {method_name}({param_str}) -> None:")
            self._indent += 1
            ctx_method = f"on_{to_snake_case(evt.id)}"
            if self.settings.asynchronous:
                self._line(f"self.context.schedule_event(self.context.{ctx_method}, {args_str})" if args_str else f"self.context.schedule_event(self.context.{ctx_method})")
            else:
                self._line(f"self.context.{ctx_method}({args_str})")
            self._indent -= 1
            self._emit_blank()

        self._indent -= 1

    def _emit_action(self, action: ActionDef, ctx_var: str) -> None:
        if action.code:
            self._line(f"{ctx_var}.{action.code}")
        elif action.timer_start:
            timer = action.timer_start.get("timer", "")
            duration = action.timer_start.get("duration", "0")
            timer_event = to_snake_case(timer)
            self._line(f'{ctx_var}.start_timer("{timer}", {duration}, lambda: {ctx_var}.on_{timer_event}())')
        elif action.timer_stop:
            self._line(f'{ctx_var}.stop_timer("{action.timer_stop}")')

    def _find_initial_leaf(self, state: StateDef) -> Optional[StateDef]:
        """Find the initial leaf state (first child of first child chain)."""
        if state.is_leaf:
            return state
        if state.states:
            return self._find_initial_leaf(state.states[0])
        return None

    def _build_entry_chain(self, state: StateDef) -> list[StateDef]:
        """Build the chain of states to enter from root to this state."""
        chain: list[StateDef] = []
        name = state.name
        while name is not None:
            s = self.model.state_map.get(name)
            if s:
                chain.append(s)
            name = self.model.parent_map.get(name)
        return list(reversed(chain))

    # --- Helpers ---

    def _line(self, text: str) -> None:
        indent = "    " * self._indent
        self.lines.append(f"{indent}{text}")

    def _emit_blank(self) -> None:
        self.lines.append("")


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def generate_python(
    input_path: str | Path,
    output_path: Optional[str | Path] = None,
) -> str:
    """
    Generate Python state machine code from a definition file.

    Args:
        input_path: Path to .smdf.json or .smdf.xml file
        output_path: Optional output file path. If None, returns code as string.

    Returns:
        Generated Python source code
    """
    parser = StateMachineParser()
    model = parser.parse_file(input_path)
    errors = parser.validate(model)
    if errors:
        error_msgs = "\n".join(f"  [{e.rule_id}] {e.message}" for e in errors)
        raise ValueError(f"Validation errors:\n{error_msgs}")

    generator = PythonCodeGenerator(model)
    code = generator.generate()

    if output_path:
        output_path = Path(output_path)
        output_path.parent.mkdir(parents=True, exist_ok=True)
        output_path.write_text(code, encoding="utf-8")

    return code
