"""
Base Node Executor

Abstract base class and utilities for all node executors.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any, TYPE_CHECKING

if TYPE_CHECKING:
    from ..schemas import Node
    from ..storage import StorageManager


class NodeExecutor(ABC):
    """
    Abstract base class for node executors.
    
    Each node type should have a corresponding executor that
    implements the execute method.
    """
    
    @abstractmethod
    def execute(
        self,
        node: "Node",
        input_data: dict[str, Any],
        storage: "StorageManager",
        run_id: str,
    ) -> Any:
        """
        Execute the node operation.
        
        Args:
            node: The node configuration
            input_data: Dictionary mapping input node IDs to their outputs
            storage: Storage manager for saving artifacts
            run_id: Current execution run ID
            
        Returns:
            Output data to pass to downstream nodes
        """
        pass
    
    def get_param(self, node: "Node", key: str, default: Any = None) -> Any:
        """Helper to get a parameter from node data."""
        return node.data.params.get(key, default)
    
    def get_single_input(self, input_data: dict[str, Any]) -> Any:
        """Get the single input when only one upstream node exists."""
        if not input_data:
            return None
        return list(input_data.values())[0]
    
    def merge_dataframes(self, input_data: dict[str, Any]) -> Any:
        """Merge multiple input DataFrames."""
        import pandas as pd
        
        dfs = [v for v in input_data.values() if isinstance(v, pd.DataFrame)]
        if not dfs:
            return None
        if len(dfs) == 1:
            return dfs[0]
        return pd.concat(dfs, ignore_index=True)


def executor_function(executor_class: type[NodeExecutor]):
    """
    Decorator to convert an executor class to a callable function.
    
    This allows the engine to call executors uniformly.
    """
    def wrapper(
        node: "Node",
        input_data: dict[str, Any],
        storage: "StorageManager",
        run_id: str,
    ) -> Any:
        executor = executor_class()
        return executor.execute(node, input_data, storage, run_id)
    
    return wrapper
