"""
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 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 = (
        start_node.function 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

        if node.is_exit:
            # Exit nodes use alias (e.g., done as _exit_success_done)
            alias = _node_to_alias(node)
            lines.append(f"from {node.module} import {node.function} as {alias}")
        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"

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


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

        if node.is_exit:
            alias = _node_to_alias(node)
            lines.append(f'{alias}._node_name = "{node.name}"  # type: ignore[attr-defined]')
        else:
            lines.append(f'{node.function}._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
                alias = _node_to_alias(target_node)
                lines.append(f'    "{state_key}": {alias},')
            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 = target_node.function
        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("-", "_")


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}()
'''
