from __future__ import annotations

import inspect
from typing import Callable, Coroutine, Generic, TypeVar

from graph.interface import BaseNode

from sera.libs.digraph._context import Context
from sera.libs.digraph._execution_summary import ExecutionSummary
from sera.libs.digraph._node_input import NodeInput
from sera.libs.digraph._node_output import NodeOutput
from sera.libs.digraph._types import NodeID

I = TypeVar("I", bound=tuple)
O = TypeVar("O")

# Type alias for sync compute function signature
SyncComputeFn = Callable[
    [NodeInput[I], Context], tuple[NodeOutput[O], ExecutionSummary]
]

# Type alias for async compute function signature
AsyncComputeFn = Callable[
    [NodeInput[I], Context],
    Coroutine[None, None, tuple[NodeOutput[O], ExecutionSummary]],
]

# Combined type for both sync and async compute functions
ComputeFn = SyncComputeFn[I, O] | AsyncComputeFn[I, O]


class Node(BaseNode[NodeID], Generic[I, O]):
    """A node in the computing graph.

    The compute function takes:
    - input: NodeInput[I] containing the item ID and arguments
    - context: Context providing access to previous outputs and global state

    And returns:
    - A tuple of (NodeOutput[O], ExecutionSummary)

    Example:
        >>> def process_order(input: NodeInput[tuple[Order]], context: Context) -> tuple[NodeOutput[ProcessedOrder], ExecutionSummary]:
        ...     order = input.args[0]
        ...     result = ProcessedOrder(...)
        ...     return NodeOutput(value=result), CreateSummary()
        ...
        >>> node = Node(id="process_order", compute=process_order)
    """

    def __init__(self, id: NodeID, compute: ComputeFn[I, O]):
        super().__init__(id)
        self.compute = compute
        self.is_async = inspect.iscoroutinefunction(compute)
