"""
Code generator for transition graphs.

Generates Python code from TransitionGraph data structures.
All generation functions are pure - they take data and return strings.

New in v0.12.0:
- Exit node support
- Aliased imports for exit nodes
- _node_name attribute assignments

v0.12.3:
- EXIT_CODES removed (ExitContract defines exit_code directly)
"""
from __future__ import annotations

from collections.abc import Sequence
from datetime import datetime

from railway.core.dag.types import NodeDefinition, TransitionGraph

# =============================================================================
# Helper Functions (Pure)
# =============================================================================


def _find_node_by_name(
    nodes: Sequence[NodeDefinition],
    name: str,
) -> NodeDefinition | None:
    """Find a node by name (pure function).

    Args:
        nodes: Sequence of NodeDefinition
        name: Node name to find

    Returns:
        NodeDefinition if found, None otherwise
    """
    return next((n for n in nodes if n.name == name), None)


def _node_to_alias(node: NodeDefinition) -> str:
    """Generate alias from node name (pure function).

    Exit nodes may have conflicting function names (e.g., multiple 'done'),
    so we use the full node path as alias.

    Example: "exit.success.done" → "_exit_success_done"

    Note:
        Underscore prefix avoids collision with regular nodes.
        Convention: regular node names don't start with "_exit_".
    """
    return "_" + node.name.replace(".", "_")


def _get_python_ref(node: NodeDefinition) -> str:
    """Get Python reference name for a node (pure function).

    Nodes with dots in their name need aliases to avoid invalid Python
    identifiers. Exit nodes always use aliases. Regular nodes with dots
    (e.g., "sub.deep.process") also use aliases.

    Args:
        node: Node definition

    Returns:
        Valid Python identifier to reference this node
    """
    if node.is_exit or "." in node.name:
        return _node_to_alias(node)
    return node.function


def generate_transition_code(graph: TransitionGraph, source_file: str) -> str:
    """Generate complete transition code file.

    This is the main entry point for code generation.
    Returns a complete, valid Python file as a string.

    Args:
        graph: Parsed transition graph
        source_file: Path to source YAML file

    Returns:
        Generated Python code as string

    Note:
        Call validate_graph(graph) before this to ensure valid output.
    """
    class_name = _to_class_name(graph.entrypoint)

    # Get start node function name
    start_node = graph.get_node(graph.start_node)
    start_function = (
        _get_python_ref(start_node) if start_node else "None  # ERROR: start node not found"
    )

    parts = [
        _generate_header(source_file),
        _generate_framework_imports(),
        generate_imports(graph),
        "",
        generate_node_name_assignments(graph),
        "",
        generate_start_node_constant(graph),
        "",
        generate_state_enum(graph),
        "",
        generate_exit_enum(graph),
        "",
        generate_transition_table(graph),
        "",
        generate_metadata(graph, source_file),
        "",
        _generate_helper_functions(class_name, start_function),
        "",
        generate_run_helper(),
    ]

    return "\n".join(parts)


def _generate_header(source_file: str) -> str:
    """Generate file header with warning."""
    timestamp = datetime.now().isoformat()
    return f"""# DO NOT EDIT - Generated by `railway sync transition`
# Source: {source_file}
# Generated at: {timestamp}
#
# This file is auto-generated from the transition graph YAML.
# Any manual changes will be overwritten on next sync.
"""


def _generate_framework_imports() -> str:
    """Generate framework imports."""
    return """from typing import Any, Callable

from railway import ExitContract
from railway.core.dag.runner import dag_runner, async_dag_runner
from railway.core.dag.state import NodeOutcome
"""


def generate_imports(graph: TransitionGraph) -> str:
    """Generate import statements for all nodes (including exit nodes).

    Exit nodes use aliased imports to avoid function name conflicts.

    Args:
        graph: Transition graph

    Returns:
        Import statements as string
    """
    lines = ["# Node imports"]
    for node in graph.nodes:
        if not node.has_handler:
            continue

        ref = _get_python_ref(node)
        if ref != node.function:
            # Nodes with dots use alias (exit nodes, deep nested nodes)
            lines.append(f"from {node.module} import {node.function} as {ref}")
        else:
            # Regular nodes
            lines.append(f"from {node.module} import {node.function}")

    return "\n".join(lines)


def generate_start_node_constant(graph: TransitionGraph) -> str:
    """Generate START_NODE constant (pure function).

    Args:
        graph: Transition graph

    Returns:
        START_NODE constant definition
    """
    start_node = _find_node_by_name(graph.nodes, graph.start_node)
    if start_node is None:
        return "# START_NODE: not found"

    ref = _get_python_ref(start_node)
    return f"# Start node (from YAML: start: {graph.start_node})\nSTART_NODE = {ref}"


def generate_run_helper() -> str:
    """Generate run() and run_async() helper functions (pure function).

    Returns:
        Helper function definitions
    """
    return '''
def run(
    initial_context: Any,
    *,
    on_step: Callable[[str, str, Any], None] | None = None,
    strict: bool = True,
    max_iterations: int = 100,
) -> ExitContract:
    """Execute this workflow (synchronous).

    Automatically starts from YAML's start node and uses TRANSITION_TABLE.

    Args:
        initial_context: Initial context to pass to start node
        on_step: Callback called for each step
        strict: Raise error on undefined states (default: True)
        max_iterations: Maximum iterations (default: 100)

    Returns:
        ExitContract: Execution result with exit_code, exit_state, context
    """
    def start_wrapper():
        return START_NODE(initial_context)
    start_wrapper._node_name = START_NODE._node_name  # type: ignore[attr-defined]
    return dag_runner(
        start=start_wrapper,
        transitions=TRANSITION_TABLE,
        on_step=on_step,
        strict=strict,
        max_iterations=max_iterations,
    )


async def run_async(
    initial_context: Any,
    *,
    on_step: Callable[[str, str, Any], None] | None = None,
    strict: bool = True,
    max_iterations: int = 100,
) -> ExitContract:
    """Execute this workflow (asynchronous).

    For workflows using async nodes.

    Args:
        initial_context: Initial context to pass to start node
        on_step: Callback called for each step
        strict: Raise error on undefined states (default: True)
        max_iterations: Maximum iterations (default: 100)

    Returns:
        ExitContract: Execution result with exit_code, exit_state, context
    """
    async def start_wrapper():
        return await START_NODE(initial_context)
    start_wrapper._node_name = START_NODE._node_name  # type: ignore[attr-defined]
    return await async_dag_runner(
        start=start_wrapper,
        transitions=TRANSITION_TABLE,
        on_step=on_step,
        strict=strict,
        max_iterations=max_iterations,
    )
'''


def generate_node_name_assignments(graph: TransitionGraph) -> str:
    """Generate _node_name attribute assignments for all nodes (pure function).

    dag_runner needs _node_name to identify nodes during execution.

    Args:
        graph: Transition graph

    Returns:
        Assignment statements as string
    """
    lines = ["# Node name attributes (for dag_runner)"]

    for node in graph.nodes:
        if not node.has_handler:
            continue

        ref = _get_python_ref(node)
        lines.append(f'{ref}._node_name = "{node.name}"  # type: ignore[attr-defined]')

    return "\n".join(lines)


def generate_state_enum(graph: TransitionGraph) -> str:
    """
    Generate state enum from graph transitions.

    Args:
        graph: Transition graph

    Returns:
        State enum class definition as string
    """
    class_name = _to_class_name(graph.entrypoint)
    lines = [
        f"class {class_name}State(NodeOutcome):",
        '    """Auto-generated state enum for this workflow."""',
    ]

    # Collect unique states per node
    for node in graph.nodes:
        states = graph.get_states_for_node(node.name)
        if states:
            lines.append(f"    # {node.name}")
            for state in states:
                enum_name = _to_enum_name(node.name, state)
                full_state = f"{node.name}::{state}"
                lines.append(f'    {enum_name} = "{full_state}"')

    if len(lines) == 2:
        lines.append("    pass  # No states defined")

    return "\n".join(lines)


def generate_exit_enum(graph: TransitionGraph) -> str:
    """
    Generate exit codes as constants (v0.12.2: simplified, no ExitOutcome).

    Args:
        graph: Transition graph

    Returns:
        Exit constants definition as string
    """
    class_name = _to_class_name(graph.entrypoint)
    lines = [
        f"# {class_name} exit codes (legacy format, for reference only)",
    ]

    for exit_def in graph.exits:
        const_name = _to_exit_enum_name(exit_def.name)
        color = "green" if exit_def.code == 0 else "red"
        lines.append(f'{const_name} = "exit::{color}::{exit_def.name}"  # code={exit_def.code}')

    if len(lines) == 1:
        lines.append("# No legacy exits defined")

    return "\n".join(lines)


def generate_transition_table(graph: TransitionGraph) -> str:
    """Generate transition table mapping state strings to next steps.

    Uses string keys for simplicity - matches Outcome-based API.
    Supports both legacy (exit::) and new (exit.) formats.

    Args:
        graph: Transition graph

    Returns:
        Transition table definition as string
    """
    lines = [
        "TRANSITION_TABLE: dict[str, Callable | str] = {",
    ]

    for transition in graph.transitions:
        # State string key: "node_name::outcome_type::detail"
        state_key = f"{transition.from_node}::{transition.from_state}"

        target = transition.to_target

        # New format: exit.* targets (v0.12.0+)
        if target.startswith("exit."):
            target_node = _find_node_by_name(graph.nodes, target)
            if target_node and target_node.has_handler:
                # Exit node with handler - use alias
                ref = _get_python_ref(target_node)
                lines.append(f'    "{state_key}": {ref},')
            continue

        # Legacy format: exit::name (generate as string for runner to handle)
        if target.startswith("exit::"):
            exit_name = transition.exit_name
            exit_def = graph.get_exit(exit_name) if exit_name else None
            if exit_def:
                color = "green" if exit_def.code == 0 else "red"
            else:
                color = "red"  # default to error
            # Generate as string literal - dag_runner handles "exit::..." strings
            lines.append(f'    "{state_key}": "exit::{color}::{exit_name}",')
            continue

        # Regular node target
        target_node = graph.get_node(target)
        if target_node:
            target_ref = _get_python_ref(target_node)
        else:
            target_ref = f'"# ERROR: unknown node {target}"'
        lines.append(f'    "{state_key}": {target_ref},')

    lines.append("}")

    return "\n".join(lines)


def generate_metadata(graph: TransitionGraph, source_file: str) -> str:
    """
    Generate graph metadata dictionary.

    Args:
        graph: Transition graph
        source_file: Path to source YAML

    Returns:
        Metadata dictionary definition as string
    """
    max_iter = graph.options.max_iterations if graph.options else 100
    timestamp = datetime.now().isoformat()

    return f'''GRAPH_METADATA = {{
    "version": "{graph.version}",
    "entrypoint": "{graph.entrypoint}",
    "description": "{graph.description}",
    "source_file": "{source_file}",
    "generated_at": "{timestamp}",
    "start_node": "{graph.start_node}",
    "max_iterations": {max_iter},
}}'''


def _generate_helper_functions(class_name: str, start_function: str) -> str:
    """
    Generate helper functions.

    Args:
        class_name: PascalCase class name prefix
        start_function: Name of the start node function
    """
    state_class = f"{class_name}State"

    return f'''
def get_next_step(state: {state_class}) -> Callable | str:
    """
    Get the next step for a given state.

    Args:
        state: Current state from node execution

    Returns:
        Next node function or exit string

    Raises:
        KeyError: If state is not in transition table
    """
    if state not in TRANSITION_TABLE:
        raise KeyError(f"未定義の状態です: {{state}}")
    return TRANSITION_TABLE[state]


def get_start_node() -> Callable:
    """Get the start node function."""
    return {start_function}
'''


def _to_enum_name(node_name: str, state: str) -> str:
    """
    Convert node name and state to enum member name.

    Example: ("fetch", "success::done") -> "FETCH_SUCCESS_DONE"
    """
    combined = f"{node_name}_{state}"
    return combined.upper().replace("::", "_").replace("-", "_").replace(".", "_")


def _to_class_name(entrypoint: str) -> str:
    """
    Convert entrypoint name to class name.

    Example: "my_workflow" -> "MyWorkflow"
    """
    return "".join(word.capitalize() for word in entrypoint.split("_"))


def _to_exit_enum_name(exit_name: str) -> str:
    """
    Convert exit name to enum member name.

    Example: "green_resolved" -> "GREEN_RESOLVED"
    """
    return exit_name.upper().replace("-", "_")


# =============================================================================
# Context Type Detection
# =============================================================================


def detect_context_type(module_path: str, function_name: str) -> str | None:
    """開始ノードからコンテキスト型を検出する（純粋関数）。

    開始ノード関数の第一引数の型ヒントを解析し、
    コンテキスト型名を返す。

    Args:
        module_path: モジュールパス（例: "nodes.start"）
        function_name: 関数名（例: "start"）

    Returns:
        コンテキスト型名（検出できない場合は None）

    Example:
        >>> # nodes/start.py:
        >>> # def start(ctx: MyContext) -> tuple[MyContext, Outcome]: ...
        >>> detect_context_type("nodes.start", "start")
        'MyContext'
    """
    try:
        import importlib
        import inspect
        from typing import get_type_hints

        # キャッシュを無効化（テスト時の動的モジュール作成に対応）
        importlib.invalidate_caches()

        module = importlib.import_module(module_path)
        func = getattr(module, function_name)

        # 型ヒントを取得
        hints = get_type_hints(func)

        # 第一引数の型を取得
        sig = inspect.signature(func)
        params = list(sig.parameters.keys())

        if not params:
            return None

        first_param = params[0]
        if first_param not in hints:
            return None

        ctx_type = hints[first_param]
        if hasattr(ctx_type, "__name__"):
            return str(ctx_type.__name__)
        return None

    except Exception:
        return None


# =============================================================================
# Exit Node Skeleton Generation (Issue #44)
# =============================================================================


def _exit_path_to_contract_name(exit_path: str) -> str:
    """終端ノードパスから Contract クラス名を生成（純粋関数）。

    Args:
        exit_path: "exit.success.done" 形式のパス

    Returns:
        "SuccessDoneResult" 形式のクラス名

    Examples:
        >>> _exit_path_to_contract_name("exit.success.done")
        'SuccessDoneResult'
        >>> _exit_path_to_contract_name("exit.failure.ssh.handshake")
        'FailureSshHandshakeResult'
    """
    # "exit." を除去し、各パートを PascalCase に変換
    parts = exit_path.replace("exit.", "", 1).split(".")
    pascal_parts = [part.capitalize() for part in parts]
    return "".join(pascal_parts) + "Result"


def _exit_path_to_exit_state(exit_path: str) -> str:
    """終端ノードパスから exit_state を生成（純粋関数）。

    Args:
        exit_path: "exit.success.done" 形式のパス

    Returns:
        "success.done" 形式の exit_state
    """
    return exit_path.replace("exit.", "", 1)


def generate_exit_node_skeleton(node: NodeDefinition) -> str:
    """終端ノードのスケルトンコードを生成（純粋関数）。

    Args:
        node: 終端ノード定義

    Returns:
        生成された Python コード文字列

    Note:
        生成されたコードは構文的に正しく、型チェックを通過する。
        開発者は TODO コメントの部分を実装するだけでよい。

        ctx の型は ExitContract としている。これは：
        - Any より型安全
        - 開発者が具体的な型に変更可能
        - IDE 補完が効く
    """
    contract_name = _exit_path_to_contract_name(node.name)
    exit_state = _exit_path_to_exit_state(node.name)
    function_name = node.function

    return f'''"""終端ノード: {node.description}

Auto-generated by `railway sync transition`.
"""
from railway import ExitContract, node


class {contract_name}(ExitContract):
    """{node.description}の結果。

    TODO: カスタムフィールドを追加してください。
    例:
        processed_count: int
        summary: str
    """
    exit_state: str = "{exit_state}"


@node(name="{node.name}")
def {function_name}(ctx: ExitContract) -> {contract_name}:
    """{node.description}

    Args:
        ctx: 直前のノードからのコンテキスト

    Returns:
        {contract_name}: 終了結果

    TODO: 実装してください。
    """
    return {contract_name}()
'''
