"""Unified Runtime Engine with Enterprise Capabilities.

This module provides a unified, production-ready execution engine that seamlessly
integrates all enterprise features through the composable node architecture. It
combines sync/async execution, enterprise security, monitoring, and resource
management - all implemented through existing enterprise nodes and SDK patterns.

Examples:
    Basic workflow execution (backward compatible):

    >>> from kailash.runtime.local import LocalRuntime
    >>> runtime = LocalRuntime(debug=True, enable_cycles=True)
    >>> results, run_id = runtime.execute(workflow, parameters={"input": "data"})

    Enterprise configuration with security:

    >>> from kailash.access_control import UserContext
    >>> user_context = UserContext(user_id="user123", roles=["analyst"])
    >>> runtime = LocalRuntime(
    ...     user_context=user_context,
    ...     enable_monitoring=True,
    ...     enable_security=True
    ... )
    >>> results, run_id = runtime.execute(workflow, parameters={"data": input_data})

    Full enterprise features:

    >>> runtime = LocalRuntime(
    ...     enable_async=True,           # Async node execution
    ...     enable_monitoring=True,      # Performance tracking
    ...     enable_security=True,        # Access control
    ...     enable_audit=True,           # Compliance logging
    ...     max_concurrency=10           # Parallel execution
    ... )
"""

import asyncio
import hashlib
import logging
import time
from collections import defaultdict
from datetime import UTC, datetime
from typing import Any, Dict, List, Optional, Set, Tuple, Union

import networkx as nx
import psutil

from kailash.nodes import Node
from kailash.runtime.compatibility_reporter import CompatibilityReporter
from kailash.runtime.parameter_injector import WorkflowParameterInjector
from kailash.runtime.performance_monitor import ExecutionMetrics, PerformanceMonitor
from kailash.runtime.secret_provider import EnvironmentSecretProvider, SecretProvider
from kailash.runtime.validation.connection_context import ConnectionContext
from kailash.runtime.validation.enhanced_error_formatter import EnhancedErrorFormatter
from kailash.runtime.validation.error_categorizer import ErrorCategorizer
from kailash.runtime.validation.metrics import (
    ValidationEventType,
    get_metrics_collector,
)
from kailash.runtime.validation.suggestion_engine import ValidationSuggestionEngine
from kailash.sdk_exceptions import (
    RuntimeExecutionError,
    WorkflowExecutionError,
    WorkflowValidationError,
)
from kailash.tracking import TaskManager, TaskStatus
from kailash.tracking.metrics_collector import MetricsCollector
from kailash.tracking.models import TaskMetrics
from kailash.workflow import Workflow
from kailash.workflow.contracts import ConnectionContract, ContractValidator
from kailash.workflow.cyclic_runner import CyclicWorkflowExecutor

logger = logging.getLogger(__name__)


class ContentAwareExecutionError(Exception):
    """Exception raised when content-aware success detection identifies a failure."""

    pass


def detect_success(result):
    """Detect success or failure from a node execution result."""
    # Handle None result (backward compatibility)
    if result is None:
        return True, None

    # Handle non-dict results (backward compatibility)
    if not isinstance(result, dict):
        return True, None

    # Handle empty dict (backward compatibility)
    if not result:
        return True, None

    # Check for success field
    if "success" not in result:
        # No success field, default to success (backward compatibility)
        return True, None

    success_value = result["success"]

    # Evaluate success value as boolean
    is_success = bool(success_value)

    if is_success:
        # Operation succeeded
        return True, None
    else:
        # Operation failed, extract error information
        error_info = result.get("error", "Operation failed (no error details provided)")
        return False, error_info


def should_stop_on_content_failure(result, content_aware_mode=True, stop_on_error=True):
    """Check if execution should stop based on content indicating failure."""
    if not content_aware_mode or not stop_on_error:
        return False, None

    # Use detect_success for the actual detection logic
    is_success, error_info = detect_success(result)

    if is_success:
        # Operation succeeded, continue execution
        return False, None
    else:
        # Operation failed, stop execution
        return True, error_info


def create_content_aware_error(node_id, result, error_message=None):
    """Create a ContentAwareExecutionError from node result."""
    if error_message is None:
        error_message = result.get("error", "Operation failed")

    error = ContentAwareExecutionError(
        f"Node '{node_id}' reported failure: {error_message}"
    )
    error.node_id = node_id
    error.failure_data = result
    return error


# Conditional execution imports (lazy-loaded to avoid circular imports)
_ConditionalBranchAnalyzer = None
_DynamicExecutionPlanner = None


def _get_conditional_analyzer():
    """Lazy import ConditionalBranchAnalyzer to avoid circular imports."""
    global _ConditionalBranchAnalyzer
    if _ConditionalBranchAnalyzer is None:
        from kailash.analysis.conditional_branch_analyzer import (
            ConditionalBranchAnalyzer,
        )

        _ConditionalBranchAnalyzer = ConditionalBranchAnalyzer
    return _ConditionalBranchAnalyzer


def _get_execution_planner():
    """Lazy import DynamicExecutionPlanner to avoid circular imports."""
    global _DynamicExecutionPlanner
    if _DynamicExecutionPlanner is None:
        from kailash.planning.dynamic_execution_planner import DynamicExecutionPlanner

        _DynamicExecutionPlanner = DynamicExecutionPlanner
    return _DynamicExecutionPlanner


class LocalRuntime:
    """Unified runtime with enterprise capabilities.

    This class provides a comprehensive, production-ready execution engine that
    seamlessly handles both traditional workflows and advanced cyclic patterns,
    with full enterprise feature integration through composable nodes.

    Enterprise Features (Composably Integrated):
    - Access control via existing AccessControlManager and security nodes
    - Real-time monitoring via TaskManager and MetricsCollector
    - Audit logging via AuditLogNode and SecurityEventNode
    - Resource management via enterprise monitoring nodes
    - Async execution support for AsyncNode instances
    - Performance optimization via PerformanceBenchmarkNode
    """

    def __init__(
        self,
        debug: bool = False,
        enable_cycles: bool = True,
        enable_async: bool = True,
        max_concurrency: int = 10,
        user_context: Optional[Any] = None,
        enable_monitoring: bool = True,
        enable_security: bool = False,
        enable_audit: bool = False,
        resource_limits: Optional[dict[str, Any]] = None,
        secret_provider: Optional[Any] = None,
        connection_validation: str = "warn",
        conditional_execution: str = "route_data",
        content_aware_success_detection: bool = True,
    ):
        """Initialize the unified runtime.

        Args:
            debug: Whether to enable debug logging.
            enable_cycles: Whether to enable cyclic workflow support.
            enable_async: Whether to enable async execution for async nodes.
            max_concurrency: Maximum concurrent async operations.
            user_context: User context for access control (optional).
            enable_monitoring: Whether to enable performance monitoring.
            enable_security: Whether to enable security features.
            enable_audit: Whether to enable audit logging.
            resource_limits: Resource limits (memory_mb, cpu_cores, etc.).
            secret_provider: Optional secret provider for runtime secret injection.
            connection_validation: Connection parameter validation mode:
                - "off": No validation (backward compatibility)
                - "warn": Log warnings on validation errors (default)
                - "strict": Raise errors on validation failures
            conditional_execution: Execution strategy for conditional routing:
                - "route_data": Current behavior - all nodes execute, data routing only (default)
                - "skip_branches": New behavior - skip unreachable branches entirely
            content_aware_success_detection: Whether to enable content-aware success detection:
                - True: Check return value content for success/failure patterns (default)
                - False: Only use exception-based failure detection (legacy mode)
        """
        # Validate connection_validation parameter
        valid_conn_modes = {"off", "warn", "strict"}
        if connection_validation not in valid_conn_modes:
            raise ValueError(
                f"Invalid connection_validation mode: {connection_validation}. "
                f"Must be one of: {valid_conn_modes}"
            )

        # Validate conditional_execution parameter
        valid_exec_modes = {"route_data", "skip_branches"}
        if conditional_execution not in valid_exec_modes:
            raise ValueError(
                f"Invalid conditional_execution mode: {conditional_execution}. "
                f"Must be one of: {valid_exec_modes}"
            )

        self.debug = debug
        self.enable_cycles = enable_cycles
        self.enable_async = enable_async
        self.max_concurrency = max_concurrency
        self.user_context = user_context
        self.secret_provider = secret_provider
        self.enable_monitoring = enable_monitoring
        self.enable_security = enable_security
        self.enable_audit = enable_audit
        self.resource_limits = resource_limits or {}
        self.connection_validation = connection_validation
        self.conditional_execution = conditional_execution
        self.content_aware_success_detection = content_aware_success_detection
        self.logger = logger

        # Enterprise feature managers (lazy initialization)
        self._access_control_manager = None

        # Initialize cyclic workflow executor if enabled
        if enable_cycles:
            self.cyclic_executor = CyclicWorkflowExecutor()

        # Initialize conditional execution components (lazy initialization)
        self._conditional_branch_analyzer = None
        self._dynamic_execution_planner = None

        # Phase 3: Basic Integration features
        self._performance_monitor = None
        self._compatibility_reporter = None
        self._enable_performance_monitoring = False
        self._performance_switch_enabled = False
        self._enable_compatibility_reporting = False

        # Phase 5: Production readiness features
        self._execution_plan_cache = {}
        self._performance_metrics = {}
        self._fallback_metrics = {}
        self._analytics_data = {
            "conditional_executions": [],
            "performance_history": [],
            "cache_hits": 0,
            "cache_misses": 0,
            "execution_patterns": {},
            "optimization_stats": {},
        }

        # Configure logging
        if debug:
            self.logger.setLevel(logging.DEBUG)
        else:
            self.logger.setLevel(logging.INFO)

        # Enterprise execution context
        self._execution_context = {
            "security_enabled": enable_security,
            "monitoring_enabled": enable_monitoring,
            "audit_enabled": enable_audit,
            "async_enabled": enable_async,
            "resource_limits": self.resource_limits,
            "user_context": user_context,
        }

    def _extract_secret_requirements(self, workflow: "Workflow") -> list:
        """Extract secret requirements from workflow nodes.

        Args:
            workflow: Workflow to analyze

        Returns:
            List of secret requirements
        """
        requirements = []
        for node_id, node in workflow.nodes.items():
            if hasattr(node, "get_secret_requirements"):
                node_requirements = node.get_secret_requirements()
                requirements.extend(node_requirements)
        return requirements

    def execute(
        self,
        workflow: Workflow,
        task_manager: TaskManager | None = None,
        parameters: dict[str, dict[str, Any]] | dict[str, Any] | None = None,
    ) -> tuple[dict[str, Any], str | None]:
        """Execute a workflow with unified enterprise capabilities.

        Args:
            workflow: Workflow to execute.
            task_manager: Optional task manager for tracking.
            parameters: Optional parameter overrides per node.

        Returns:
            Tuple of (results dict, run_id).

        Raises:
            RuntimeExecutionError: If execution fails.
            WorkflowValidationError: If workflow is invalid.
            PermissionError: If access control denies execution.
        """
        # For backward compatibility, run the async version in a sync wrapper
        try:
            # Check if we're already in an event loop
            loop = asyncio.get_running_loop()
            # If we're in an event loop, run synchronously instead
            return self._execute_sync(
                workflow=workflow, task_manager=task_manager, parameters=parameters
            )
        except RuntimeError:
            # No event loop running, safe to use asyncio.run
            return asyncio.run(
                self._execute_async(
                    workflow=workflow, task_manager=task_manager, parameters=parameters
                )
            )

    async def execute_async(
        self,
        workflow: Workflow,
        task_manager: TaskManager | None = None,
        parameters: dict[str, dict[str, Any]] | dict[str, Any] | None = None,
    ) -> tuple[dict[str, Any], str | None]:
        """Execute a workflow asynchronously (for AsyncLocalRuntime compatibility).

        Args:
            workflow: Workflow to execute.
            task_manager: Optional task manager for tracking.
            parameters: Optional parameter overrides per node.

        Returns:
            Tuple of (results dict, run_id).

        Raises:
            RuntimeExecutionError: If execution fails.
            WorkflowValidationError: If workflow is invalid.
            PermissionError: If access control denies execution.
        """
        return await self._execute_async(
            workflow=workflow, task_manager=task_manager, parameters=parameters
        )

    def _execute_sync(
        self,
        workflow: Workflow,
        task_manager: TaskManager | None = None,
        parameters: dict[str, dict[str, Any]] | dict[str, Any] | None = None,
    ) -> tuple[dict[str, Any], str | None]:
        """Execute workflow synchronously when already in an event loop.

        This method creates a new event loop in a separate thread to avoid
        conflicts with existing event loops. This ensures backward compatibility
        when LocalRuntime.execute() is called from within async contexts.

        Args:
            workflow: Workflow to execute.
            task_manager: Optional task manager for tracking.
            parameters: Optional parameter overrides per node.

        Returns:
            Tuple of (results dict, run_id).

        Raises:
            RuntimeExecutionError: If execution fails.
            WorkflowValidationError: If workflow is invalid.
        """
        # Create new event loop for sync execution
        import threading

        result_container = []
        exception_container = []

        def run_in_thread():
            """Run async execution in separate thread."""
            loop = None
            try:
                # Create new event loop in thread
                loop = asyncio.new_event_loop()
                asyncio.set_event_loop(loop)
                result = loop.run_until_complete(
                    self._execute_async(
                        workflow=workflow,
                        task_manager=task_manager,
                        parameters=parameters,
                    )
                )
                result_container.append(result)
            except Exception as e:
                exception_container.append(e)
            finally:
                if loop:
                    loop.close()

        thread = threading.Thread(target=run_in_thread)
        thread.start()
        thread.join()

        if exception_container:
            raise exception_container[0]

        return result_container[0]

    async def _execute_async(
        self,
        workflow: Workflow,
        task_manager: TaskManager | None = None,
        parameters: dict[str, dict[str, Any]] | dict[str, Any] | None = None,
    ) -> tuple[dict[str, Any], str | None]:
        """Core async execution implementation with enterprise features.

        This method orchestrates the entire workflow execution including:
        - Security checks via AccessControlManager (if enabled)
        - Audit logging via AuditLogNode (if enabled)
        - Performance monitoring via TaskManager/MetricsCollector
        - Async node detection and execution
        - Resource limit enforcement
        - Error handling and recovery

        Args:
            workflow: Workflow to execute.
            task_manager: Optional task manager for tracking.
            parameters: Optional parameter overrides per node.

        Returns:
            Tuple of (results dict, run_id).

        Raises:
            RuntimeExecutionError: If execution fails.
            WorkflowValidationError: If workflow is invalid.
            PermissionError: If access control denies execution.
        """
        if not workflow:
            raise RuntimeExecutionError("No workflow provided")

        run_id = None

        try:
            # Enterprise Security Check: Validate user access to workflow
            if self.enable_security and self.user_context:
                self._check_workflow_access(workflow)

            # Extract workflow context BEFORE parameter processing
            # This prevents workflow_context from being treated as a workflow-level parameter
            workflow_context = {}
            if parameters and "workflow_context" in parameters:
                workflow_context = parameters.pop("workflow_context")
                if not isinstance(workflow_context, dict):
                    workflow_context = {}

            # Store workflow context for inspection/cleanup
            self._current_workflow_context = workflow_context

            # Transform workflow-level parameters if needed
            processed_parameters = self._process_workflow_parameters(
                workflow, parameters
            )

            # Validate workflow with runtime parameters (Session 061)
            workflow.validate(runtime_parameters=processed_parameters)

            # Enterprise Audit: Log workflow execution start
            if self.enable_audit:
                await self._log_audit_event_async(
                    "workflow_execution_start",
                    {
                        "workflow_id": workflow.workflow_id,
                        "user_context": self._serialize_user_context(),
                        "parameters": processed_parameters,
                    },
                )

            # Initialize enhanced tracking with enterprise context
            if task_manager is None and self.enable_monitoring:
                task_manager = TaskManager()

            if task_manager:
                try:
                    run_id = task_manager.create_run(
                        workflow_name=workflow.name,
                        metadata={
                            "parameters": processed_parameters,
                            "debug": self.debug,
                            "runtime": "unified_enterprise",
                            "enterprise_features": self._execution_context,
                            "user_context": self._serialize_user_context(),
                        },
                    )
                except Exception as e:
                    self.logger.warning(f"Failed to create task run: {e}")
                    # Continue without tracking

            # Check for cyclic workflows and delegate accordingly
            if self.enable_cycles and workflow.has_cycles():
                self.logger.info(
                    "Cyclic workflow detected, using CyclicWorkflowExecutor"
                )
                # Use cyclic executor for workflows with cycles
                try:
                    # Pass run_id to cyclic executor if available
                    cyclic_results, cyclic_run_id = self.cyclic_executor.execute(
                        workflow, processed_parameters, task_manager, run_id
                    )
                    results = cyclic_results
                    # Update run_id if task manager is being used
                    if not run_id:
                        run_id = cyclic_run_id
                except Exception as e:
                    raise RuntimeExecutionError(
                        f"Cyclic workflow execution failed: {e}"
                    ) from e
            elif (
                self.conditional_execution == "skip_branches"
                and self._has_conditional_patterns(workflow)
            ):
                # Check for automatic mode switching based on performance
                current_mode = self.conditional_execution
                if (
                    self._enable_performance_monitoring
                    and self._performance_switch_enabled
                ):
                    should_switch, recommended_mode, reason = (
                        self._check_performance_switch(current_mode)
                    )
                    if should_switch:
                        self.logger.info(f"Switching execution mode: {reason}")
                        self.conditional_execution = recommended_mode
                        # If switching to route_data, use standard execution
                        if recommended_mode == "route_data":
                            results = await self._execute_workflow_async(
                                workflow=workflow,
                                task_manager=task_manager,
                                run_id=run_id,
                                parameters=processed_parameters or {},
                                workflow_context=workflow_context,
                            )
                        else:
                            # Continue with conditional execution
                            try:
                                results = await self._execute_conditional_approach(
                                    workflow=workflow,
                                    parameters=processed_parameters or {},
                                    task_manager=task_manager,
                                    run_id=run_id,
                                    workflow_context=workflow_context,
                                )
                            except Exception as e:
                                self.logger.warning(
                                    f"Conditional execution failed, falling back to standard execution: {e}"
                                )
                                # Fallback to standard execution
                                results = await self._execute_workflow_async(
                                    workflow=workflow,
                                    task_manager=task_manager,
                                    run_id=run_id,
                                    parameters=processed_parameters or {},
                                    workflow_context=workflow_context,
                                )
                    else:
                        # No switch recommended, continue with current mode
                        self.logger.info(
                            "Conditional workflow detected, using conditional execution optimization"
                        )
                        try:
                            results = await self._execute_conditional_approach(
                                workflow=workflow,
                                parameters=processed_parameters or {},
                                task_manager=task_manager,
                                run_id=run_id,
                                workflow_context=workflow_context,
                            )
                        except Exception as e:
                            self.logger.warning(
                                f"Conditional execution failed, falling back to standard execution: {e}"
                            )
                            # Fallback to standard execution
                            results = await self._execute_workflow_async(
                                workflow=workflow,
                                task_manager=task_manager,
                                run_id=run_id,
                                parameters=processed_parameters or {},
                                workflow_context=workflow_context,
                            )
                else:
                    # Performance monitoring disabled
                    self.logger.info(
                        "Conditional workflow detected, using conditional execution optimization"
                    )
                    try:
                        results = await self._execute_conditional_approach(
                            workflow=workflow,
                            parameters=processed_parameters or {},
                            task_manager=task_manager,
                            run_id=run_id,
                            workflow_context=workflow_context,
                        )
                    except Exception as e:
                        self.logger.warning(
                            f"Conditional execution failed, falling back to standard execution: {e}"
                        )
                        # Fallback to standard execution
                        results = await self._execute_workflow_async(
                            workflow=workflow,
                            task_manager=task_manager,
                            run_id=run_id,
                            parameters=processed_parameters or {},
                            workflow_context=workflow_context,
                        )
            else:
                # Execute standard DAG workflow with enterprise features
                execution_mode = (
                    "route_data"
                    if self.conditional_execution == "route_data"
                    else "standard"
                )
                self.logger.info(
                    f"Standard DAG workflow detected, using unified enterprise execution ({execution_mode} mode)"
                )
                results = await self._execute_workflow_async(
                    workflow=workflow,
                    task_manager=task_manager,
                    run_id=run_id,
                    parameters=processed_parameters or {},
                    workflow_context=workflow_context,
                )

            # Enterprise Audit: Log successful completion
            if self.enable_audit:
                await self._log_audit_event_async(
                    "workflow_execution_completed",
                    {
                        "workflow_id": workflow.workflow_id,
                        "run_id": run_id,
                        "result_summary": {
                            k: type(v).__name__ for k, v in results.items()
                        },
                    },
                )

            # Mark run as completed
            if task_manager and run_id:
                try:
                    task_manager.update_run_status(run_id, "completed")
                except Exception as e:
                    self.logger.warning(f"Failed to update run status: {e}")

            # Final cleanup of all node instances
            for node_id, node_instance in workflow._node_instances.items():
                if hasattr(node_instance, "cleanup"):
                    try:
                        await node_instance.cleanup()
                    except Exception as cleanup_error:
                        self.logger.warning(
                            f"Error during final cleanup of node {node_id}: {cleanup_error}"
                        )

            return results, run_id

        except WorkflowValidationError:
            # Enterprise Audit: Log validation failure
            if self.enable_audit:
                await self._log_audit_event_async(
                    "workflow_validation_failed",
                    {
                        "workflow_id": workflow.workflow_id,
                        "error": "Validation failed",
                    },
                )
            # Re-raise validation errors as-is
            if task_manager and run_id:
                try:
                    task_manager.update_run_status(
                        run_id, "failed", error="Validation failed"
                    )
                except Exception:
                    pass
            raise
        except PermissionError as e:
            # Enterprise Audit: Log access denial
            if self.enable_audit:
                await self._log_audit_event_async(
                    "workflow_access_denied",
                    {
                        "workflow_id": workflow.workflow_id,
                        "user_context": self._serialize_user_context(),
                        "error": str(e),
                    },
                )
            # Re-raise permission errors as-is
            if task_manager and run_id:
                try:
                    task_manager.update_run_status(run_id, "failed", error=str(e))
                except Exception:
                    pass
            raise
        except Exception as e:
            # Enterprise Audit: Log execution failure
            if self.enable_audit:
                await self._log_audit_event_async(
                    "workflow_execution_failed",
                    {
                        "workflow_id": workflow.workflow_id,
                        "error": str(e),
                    },
                )
            # Mark run as failed
            if task_manager and run_id:
                try:
                    task_manager.update_run_status(run_id, "failed", error=str(e))
                except Exception:
                    pass

            # Wrap other errors in RuntimeExecutionError
            raise RuntimeExecutionError(
                f"Unified enterprise workflow execution failed: {type(e).__name__}: {e}"
            ) from e

    async def _execute_workflow_async(
        self,
        workflow: Workflow,
        task_manager: TaskManager | None,
        run_id: str | None,
        parameters: dict[str, dict[str, Any]],
        workflow_context: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """Execute the workflow nodes in topological order.

        Args:
            workflow: Workflow to execute.
            task_manager: Task manager for tracking.
            run_id: Run ID for tracking.
            parameters: Parameter overrides.

        Returns:
            Dictionary of node results.

        Raises:
            WorkflowExecutionError: If execution fails.
        """
        # Get execution order
        try:
            execution_order = list(nx.topological_sort(workflow.graph))
            self.logger.info(f"Execution order: {execution_order}")
        except nx.NetworkXError as e:
            raise WorkflowExecutionError(
                f"Failed to determine execution order: {e}"
            ) from e

        # Initialize results storage
        results = {}
        node_outputs = {}
        failed_nodes = []

        # Make results available to _should_skip_conditional_node for transitive dependency checking
        self._current_results = results

        # Use the workflow context passed from _execute_async
        if workflow_context is None:
            workflow_context = {}

        # Store the workflow context for cleanup later
        self._current_workflow_context = workflow_context

        # Execute each node
        for node_id in execution_order:
            self.logger.info(f"Executing node: {node_id}")

            # Get node instance
            node_instance = workflow._node_instances.get(node_id)
            if not node_instance:
                raise WorkflowExecutionError(
                    f"Node instance '{node_id}' not found in workflow"
                )

            # Start task tracking
            task = None
            if task_manager and run_id:
                try:
                    # Get node metadata if available
                    node_metadata = {}
                    if hasattr(node_instance, "config") and isinstance(
                        node_instance.config, dict
                    ):
                        raw_metadata = node_instance.config.get("metadata", {})
                        # Convert NodeMetadata object to dict if needed
                        if hasattr(raw_metadata, "model_dump"):
                            node_metadata_dict = raw_metadata.model_dump()
                            # Convert datetime objects to strings for JSON serialization
                            if "created_at" in node_metadata_dict:
                                node_metadata_dict["created_at"] = str(
                                    node_metadata_dict["created_at"]
                                )
                            # Convert sets to lists for JSON serialization
                            if "tags" in node_metadata_dict and isinstance(
                                node_metadata_dict["tags"], set
                            ):
                                node_metadata_dict["tags"] = list(
                                    node_metadata_dict["tags"]
                                )
                            node_metadata = node_metadata_dict
                        elif isinstance(raw_metadata, dict):
                            node_metadata = raw_metadata

                    task = task_manager.create_task(
                        run_id=run_id,
                        node_id=node_id,
                        node_type=node_instance.__class__.__name__,
                        started_at=datetime.now(UTC),
                        metadata=node_metadata,
                    )
                    # Start the task
                    if task:
                        task_manager.update_task_status(
                            task.task_id, TaskStatus.RUNNING
                        )
                except Exception as e:
                    self.logger.warning(
                        f"Failed to create task for node '{node_id}': {e}"
                    )

            try:
                # Prepare inputs
                inputs = self._prepare_node_inputs(
                    workflow=workflow,
                    node_id=node_id,
                    node_instance=node_instance,
                    node_outputs=node_outputs,
                    parameters=parameters.get(node_id, {}),
                )

                # Update node config with parameters (Session 061: direct config update)
                {**node_instance.config, **parameters.get(node_id, {})}
                node_instance.config.update(parameters.get(node_id, {}))

                # ENTERPRISE PARAMETER INJECTION FIX: Injected parameters should override connection inputs
                # This ensures workflow parameters take precedence over connection inputs for the same parameter names
                injected_params = parameters.get(node_id, {})
                if injected_params:
                    inputs.update(injected_params)
                    if self.debug:
                        self.logger.debug(
                            f"Applied parameter injections for {node_id}: {list(injected_params.keys())}"
                        )

                if self.debug:
                    self.logger.debug(f"Node {node_id} inputs: {inputs}")

                # CONDITIONAL EXECUTION: Skip nodes that only receive None inputs from conditional routing
                if self._should_skip_conditional_node(workflow, node_id, inputs):
                    if self.debug:
                        self.logger.debug(
                            f"DEBUG: Skipping {node_id} - inputs: {inputs}"
                        )
                    self.logger.info(
                        f"Skipping node {node_id} - all conditional inputs are None"
                    )
                    # Store None result to indicate the node was skipped
                    results[node_id] = None
                    node_outputs[node_id] = None

                    # Update task status if tracking is enabled
                    if task and task_manager:
                        task_manager.update_task_status(
                            task.task_id,
                            TaskStatus.COMPLETED,
                            result=None,
                            ended_at=datetime.now(UTC),
                            metadata={"skipped": True, "reason": "conditional_routing"},
                        )
                    continue

                # Execute node with unified async/sync support and metrics collection
                collector = MetricsCollector()
                with collector.collect(node_id=node_id) as metrics_context:
                    # Unified async/sync execution
                    # Validate inputs before execution
                    from kailash.utils.data_validation import DataTypeValidator

                    validated_inputs = DataTypeValidator.validate_node_input(
                        node_id, inputs
                    )

                    # Set workflow context on the node instance
                    if hasattr(node_instance, "_workflow_context"):
                        node_instance._workflow_context = workflow_context
                    else:
                        # Initialize the workflow context if it doesn't exist
                        node_instance._workflow_context = workflow_context

                    if self.enable_async and hasattr(node_instance, "execute_async"):
                        # Use async execution method that includes validation
                        outputs = await node_instance.execute_async(**validated_inputs)
                    else:
                        # Standard synchronous execution
                        outputs = node_instance.execute(**validated_inputs)

                # Get performance metrics
                performance_metrics = metrics_context.result()

                # Store outputs
                node_outputs[node_id] = outputs
                results[node_id] = outputs

                if self.debug:
                    self.logger.debug(f"Node {node_id} outputs: {outputs}")

                # Content-aware success detection (CRITICAL FIX)
                if self.content_aware_success_detection:
                    should_stop, error_message = should_stop_on_content_failure(
                        result=outputs,
                        content_aware_mode=True,
                        stop_on_error=True,  # Always stop on content failures when content-aware mode is enabled
                    )

                    if should_stop:
                        # Create detailed error for content-aware failure
                        error = create_content_aware_error(
                            node_id=node_id,
                            result=(
                                outputs
                                if isinstance(outputs, dict)
                                else {"error": error_message}
                            ),
                            error_message=error_message,
                        )

                        # Log the content-aware failure
                        self.logger.error(
                            f"Content-aware failure detected in node {node_id}: {error_message}"
                        )

                        # Update task status to failed if task manager exists
                        if task and task_manager:
                            task_manager.update_task_status(
                                task.task_id,
                                TaskStatus.FAILED,
                                error=str(error),
                                ended_at=datetime.now(UTC),
                            )

                        # Raise the content-aware execution error
                        raise error

                # Update task status with enhanced metrics
                if task and task_manager:
                    # Convert performance metrics to TaskMetrics format
                    task_metrics_data = performance_metrics.to_task_metrics()
                    task_metrics = TaskMetrics(**task_metrics_data)

                    # Update task with metrics
                    task_manager.update_task_status(
                        task.task_id,
                        TaskStatus.COMPLETED,
                        result=outputs,
                        ended_at=datetime.now(UTC),
                        metadata={"execution_time": performance_metrics.duration},
                    )

                    # Update task metrics separately
                    task_manager.update_task_metrics(task.task_id, task_metrics)

                self.logger.info(
                    f"Node {node_id} completed successfully in {performance_metrics.duration:.3f}s"
                )

                # Clean up async resources if the node has a cleanup method
                if hasattr(node_instance, "cleanup"):
                    try:
                        await node_instance.cleanup()
                    except Exception as cleanup_error:
                        self.logger.warning(
                            f"Error during node {node_id} cleanup: {cleanup_error}"
                        )

            except Exception as e:
                failed_nodes.append(node_id)
                self.logger.error(f"Node {node_id} failed: {e}", exc_info=self.debug)

                # Update task status
                if task and task_manager:
                    task_manager.update_task_status(
                        task.task_id,
                        TaskStatus.FAILED,
                        error=str(e),
                        ended_at=datetime.now(UTC),
                    )

                # Clean up async resources even on failure
                if hasattr(node_instance, "cleanup"):
                    try:
                        await node_instance.cleanup()
                    except Exception as cleanup_error:
                        self.logger.warning(
                            f"Error during node {node_id} cleanup after failure: {cleanup_error}"
                        )

                # Content-aware execution errors should always stop execution
                if isinstance(e, ContentAwareExecutionError):
                    error_msg = f"Content-aware failure in node '{node_id}': {e}"
                    raise WorkflowExecutionError(error_msg) from e

                # Determine if we should continue for other exceptions
                if self._should_stop_on_error(workflow, node_id):
                    error_msg = f"Node '{node_id}' failed: {e}"
                    if len(failed_nodes) > 1:
                        error_msg += f" (Previously failed nodes: {failed_nodes[:-1]})"

                    raise WorkflowExecutionError(error_msg) from e
                else:
                    # Continue execution but record error
                    results[node_id] = {
                        "error": str(e),
                        "error_type": type(e).__name__,
                        "failed": True,
                    }

        # Clean up workflow context
        self._current_workflow_context = None

        return results

    def _prepare_node_inputs(
        self,
        workflow: Workflow,
        node_id: str,
        node_instance: Node,
        node_outputs: dict[str, dict[str, Any]],
        parameters: dict[str, Any],
    ) -> dict[str, Any]:
        """Prepare inputs for a node execution.

        Args:
            workflow: The workflow being executed.
            node_id: Current node ID.
            node_instance: Current node instance.
            node_outputs: Outputs from previously executed nodes.
            parameters: Parameter overrides.

        Returns:
            Dictionary of inputs for the node.

        Raises:
            WorkflowExecutionError: If input preparation fails.
        """
        inputs = {}

        # NOTE: Node configuration is handled separately in configure() call
        # Only add runtime inputs and data from connected nodes here

        # Add runtime parameters (those not used for node configuration)
        # Map specific runtime parameters for known node types
        if "consumer_timeout_ms" in parameters:
            inputs["timeout_ms"] = parameters["consumer_timeout_ms"]

        # Add other potential runtime parameters that are not configuration
        runtime_param_names = {"max_messages", "timeout_ms", "limit", "offset"}
        for param_name, param_value in parameters.items():
            if param_name in runtime_param_names:
                inputs[param_name] = param_value

        # Add connected inputs from other nodes
        for edge in workflow.graph.in_edges(node_id, data=True):
            source_node_id = edge[0]
            mapping = edge[2].get("mapping", {})

            if self.debug:
                self.logger.debug(f"Processing edge {source_node_id} -> {node_id}")
                self.logger.debug(f"  Edge data: {edge[2]}")
                self.logger.debug(f"  Mapping: {mapping}")

            if source_node_id in node_outputs:
                source_outputs = node_outputs[source_node_id]
                if self.debug:
                    self.logger.debug(
                        f"  Source outputs: {list(source_outputs.keys())}"
                    )

                # Check if the source node failed
                if isinstance(source_outputs, dict) and source_outputs.get("failed"):
                    raise WorkflowExecutionError(
                        f"Cannot use outputs from failed node '{source_node_id}'"
                    )

                # Validate source outputs before mapping
                from kailash.utils.data_validation import DataTypeValidator

                try:
                    source_outputs = DataTypeValidator.validate_node_output(
                        source_node_id, source_outputs
                    )
                except Exception as e:
                    self.logger.warning(
                        f"Data validation failed for node '{source_node_id}': {e}"
                    )

                for source_key, target_key in mapping.items():
                    # Handle nested output access (e.g., "result.files")
                    if "." in source_key:
                        # Navigate nested structure
                        value = source_outputs
                        parts = source_key.split(".")
                        found = True

                        if self.debug:
                            self.logger.debug(f"  Navigating nested path: {source_key}")
                            self.logger.debug(f"  Starting value: {value}")

                        for i, part in enumerate(parts):
                            if isinstance(value, dict) and part in value:
                                value = value[part]
                                if self.debug:
                                    self.logger.debug(
                                        f"    Part '{part}' found, value type: {type(value)}"
                                    )
                            else:
                                # Check if it's a direct key in source_outputs (for backwards compatibility)
                                if i == 0 and source_key in source_outputs:
                                    value = source_outputs[source_key]
                                    if self.debug:
                                        self.logger.debug(
                                            f"    Found direct key '{source_key}' in source_outputs"
                                        )
                                    break
                                else:
                                    found = False
                                    if self.debug:
                                        self.logger.debug(
                                            f"  MISSING: Nested path '{source_key}' - failed at part '{part}'"
                                        )
                                        self.logger.debug(
                                            f"    Current value type: {type(value)}"
                                        )
                                        if isinstance(value, dict):
                                            self.logger.debug(
                                                f"    Available keys: {list(value.keys())}"
                                            )
                                    self.logger.warning(
                                        f"Source output '{source_key}' not found in node '{source_node_id}'. "
                                        f"Available outputs: {list(source_outputs.keys())}"
                                    )
                                    break

                        if found:
                            # CONDITIONAL EXECUTION FIX: Don't overwrite existing non-None values with None
                            # This handles cases where multiple edges map to the same input parameter
                            if (
                                target_key in inputs
                                and inputs[target_key] is not None
                                and value is None
                            ):
                                if self.debug:
                                    self.logger.debug(
                                        f"  SKIP: Not overwriting existing non-None value for {target_key} with None from {source_node_id}"
                                    )
                            else:
                                inputs[target_key] = value
                                if self.debug:
                                    self.logger.debug(
                                        f"  MAPPED: {source_key} -> {target_key} (type: {type(value)})"
                                    )
                    else:
                        # Simple key mapping
                        if source_key in source_outputs:
                            value = source_outputs[source_key]
                            # CONDITIONAL EXECUTION FIX: Don't overwrite existing non-None values with None
                            # This handles cases where multiple edges map to the same input parameter
                            if (
                                target_key in inputs
                                and inputs[target_key] is not None
                                and value is None
                            ):
                                if self.debug:
                                    self.logger.debug(
                                        f"  SKIP: Not overwriting existing non-None value for {target_key} with None from {source_node_id}"
                                    )
                            else:
                                inputs[target_key] = value
                                if self.debug:
                                    self.logger.debug(
                                        f"  MAPPED: {source_key} -> {target_key} (type: {type(value)})"
                                    )
                        else:
                            if self.debug:
                                self.logger.debug(
                                    f"  MISSING: {source_key} not in {list(source_outputs.keys())}"
                                )
                            self.logger.warning(
                                f"Source output '{source_key}' not found in node '{source_node_id}'. "
                                f"Available outputs: {list(source_outputs.keys())}"
                            )
            else:
                if self.debug:
                    self.logger.debug(
                        f"  No outputs found for source node {source_node_id}"
                    )

        # Apply parameter overrides
        inputs.update(parameters)

        # Connection parameter validation (TODO-121) with enhanced error messages and metrics
        if self.connection_validation != "off":
            metrics_collector = get_metrics_collector()
            node_type = type(node_instance).__name__

            # Start metrics collection
            metrics_collector.start_validation(
                node_id, node_type, self.connection_validation
            )

            try:
                # Phase 2: Contract validation (if contracts exist in workflow metadata)
                contract_violations = self._validate_connection_contracts(
                    workflow, node_id, inputs, node_outputs
                )

                if contract_violations:
                    contract_error_msg = "\n".join(
                        [
                            f"Contract '{violation['contract']}' violation on connection {violation['connection']}: {violation['error']}"
                            for violation in contract_violations
                        ]
                    )
                    raise WorkflowExecutionError(
                        f"Connection contract validation failed for node '{node_id}': {contract_error_msg}"
                    )

                # Merge node config with inputs before validation (matches node.execute behavior)
                # This ensures connection validation considers both runtime inputs AND node configuration
                merged_inputs = {**node_instance.config, **inputs}

                # Handle nested config case (same as in node.execute)
                if "config" in merged_inputs and isinstance(
                    merged_inputs["config"], dict
                ):
                    nested_config = merged_inputs["config"]
                    for key, value in nested_config.items():
                        if key not in inputs:  # Runtime inputs take precedence
                            merged_inputs[key] = value

                # Use the node's existing validate_inputs method with merged inputs
                validated_inputs = node_instance.validate_inputs(**merged_inputs)

                # Extract only the runtime inputs from validated results
                # (exclude config parameters that were merged for validation)
                validated_runtime_inputs = {}
                for key, value in validated_inputs.items():
                    # Include if it was in original inputs OR not in node config
                    # This preserves validated/converted values from runtime inputs
                    if key in inputs or key not in node_instance.config:
                        validated_runtime_inputs[key] = value

                # Record successful validation
                metrics_collector.end_validation(node_id, node_type, success=True)

                # Replace inputs with validated runtime inputs only
                inputs = validated_runtime_inputs

            except Exception as e:
                # Categorize the error for metrics
                categorizer = ErrorCategorizer()
                error_category = categorizer.categorize_error(e, node_type)

                # Build connection info for metrics
                connection_info = {"source": "unknown", "target": node_id}
                for connection in workflow.connections:
                    if connection.target_node == node_id:
                        connection_info["source"] = connection.source_node
                        break

                # Record failed validation
                metrics_collector.end_validation(
                    node_id,
                    node_type,
                    success=False,
                    error_category=error_category,
                    connection_info=connection_info,
                )

                # Check for security violations
                if error_category.value == "security_violation":
                    metrics_collector.record_security_violation(
                        node_id,
                        node_type,
                        {"message": str(e), "category": "connection_validation"},
                        connection_info,
                    )

                # Generate enhanced error message with connection tracing
                error_msg = self._generate_enhanced_validation_error(
                    node_id, node_instance, e, workflow, parameters
                )

                if self.connection_validation == "strict":
                    # Strict mode: raise the error with enhanced message
                    raise WorkflowExecutionError(error_msg) from e
                elif self.connection_validation == "warn":
                    # Warn mode: log enhanced warning and continue with unvalidated inputs
                    self.logger.warning(error_msg)
                    # Continue with original inputs
        else:
            # Record mode bypass for metrics
            metrics_collector = get_metrics_collector()
            metrics_collector.record_mode_bypass(
                node_id, type(node_instance).__name__, self.connection_validation
            )

        return inputs

    def _generate_enhanced_validation_error(
        self,
        node_id: str,
        node_instance: Node,
        original_error: Exception,
        workflow: "Workflow",  # Type annotation as string to avoid circular import
        parameters: dict,
    ) -> str:
        """Generate enhanced validation error message with connection tracing and suggestions.

        Args:
            node_id: ID of the target node that failed validation
            node_instance: The node instance that failed
            original_error: Original validation exception
            workflow: The workflow being executed
            parameters: Runtime parameters

        Returns:
            Enhanced error message with connection context and actionable suggestions
        """
        # Initialize error enhancement components
        categorizer = ErrorCategorizer()
        suggestion_engine = ValidationSuggestionEngine()
        formatter = EnhancedErrorFormatter()

        # Categorize the error
        node_type = type(node_instance).__name__
        error_category = categorizer.categorize_error(original_error, node_type)

        # Build connection context by finding the connections that feed into this node
        connection_context = self._build_connection_context(
            node_id, workflow, parameters
        )

        # Generate suggestion for fixing the error
        suggestion = suggestion_engine.generate_suggestion(
            error_category, node_type, connection_context, str(original_error)
        )

        # Format the enhanced error message
        if error_category.value == "security_violation":
            enhanced_msg = formatter.format_security_error(
                str(original_error), connection_context, suggestion
            )
        else:
            enhanced_msg = formatter.format_enhanced_error(
                str(original_error), error_category, connection_context, suggestion
            )

        return enhanced_msg

    def _build_connection_context(
        self, target_node_id: str, workflow: "Workflow", parameters: dict
    ) -> ConnectionContext:
        """Build connection context for error message enhancement.

        Args:
            target_node_id: ID of the target node
            workflow: The workflow being executed
            parameters: Runtime parameters

        Returns:
            ConnectionContext with source/target information
        """
        # Find the primary connection feeding into this node
        source_node = "unknown"
        source_port = None
        target_port = "input"
        parameter_value = None

        # Look through workflow connections to find what feeds this node
        for connection in workflow.connections:
            if connection.target_node == target_node_id:
                source_node = connection.source_node
                source_port = connection.source_output
                target_port = connection.target_input

                # Try to get the actual parameter value from runtime parameters
                if target_port in parameters:
                    parameter_value = parameters[target_port]
                break

        # If no connection found, this might be a direct parameter issue
        if source_node == "unknown" and parameters:
            # Find the first parameter that might have caused the issue
            for key, value in parameters.items():
                parameter_value = value
                target_port = key
                break

        return ConnectionContext(
            source_node=source_node,
            source_port=source_port,
            target_node=target_node_id,
            target_port=target_port,
            parameter_value=parameter_value,
            validation_mode=self.connection_validation,
        )

    def get_validation_metrics(self) -> Dict[str, Any]:
        """Get validation performance metrics for the runtime.

        Returns:
            Dictionary containing performance and security metrics
        """
        metrics_collector = get_metrics_collector()
        return {
            "performance_summary": metrics_collector.get_performance_summary(),
            "security_report": metrics_collector.get_security_report(),
            "raw_metrics": metrics_collector.export_metrics() if self.debug else None,
        }

    def reset_validation_metrics(self) -> None:
        """Reset validation metrics collector."""
        metrics_collector = get_metrics_collector()
        metrics_collector.reset_metrics()

    def _should_skip_conditional_node(
        self, workflow: Workflow, node_id: str, inputs: dict[str, Any]
    ) -> bool:
        """Determine if a node should be skipped due to conditional routing.

        A node should be skipped if:
        1. It has incoming connections from conditional nodes (like SwitchNode)
        2. All of its connected inputs are None
        3. It has no node-level configuration parameters that would make it run independently

        Args:
            workflow: The workflow being executed.
            node_id: Node ID to check.
            inputs: Prepared inputs for the node.

        Returns:
            True if the node should be skipped, False otherwise.
        """
        # Get all incoming edges for this node
        incoming_edges = list(workflow.graph.in_edges(node_id, data=True))

        # If the node has no incoming connections, don't skip it
        # (it might be a source node or have configuration parameters)
        if not incoming_edges:
            return False

        # Check for conditional inputs and analyze the nature of the data
        has_conditional_inputs = False
        has_non_none_connected_input = False

        for source_node_id, _, edge_data in incoming_edges:
            source_node = workflow._node_instances.get(source_node_id)
            mapping = edge_data.get("mapping", {})

            # Check if this edge provides any non-None inputs
            for source_key, target_key in mapping.items():
                if target_key in inputs and inputs[target_key] is not None:
                    has_non_none_connected_input = True

            # Direct connection from SwitchNode
            if source_node and source_node.__class__.__name__ in ["SwitchNode"]:
                has_conditional_inputs = True
            # Transitive dependency: source node was skipped due to conditional routing
            elif (
                hasattr(self, "_current_results")
                and source_node_id in self._current_results
            ):
                if self._current_results[source_node_id] is None:
                    has_conditional_inputs = True

        # If no conditional inputs, don't skip
        if not has_conditional_inputs:
            return False

        # If we have conditional inputs but also have non-None data, don't skip
        # This handles mixed scenarios where some inputs are skipped but others provide data
        if has_non_none_connected_input:
            return False

        # Get the node instance to check for configuration parameters
        node_instance = workflow._node_instances.get(node_id)
        if not node_instance:
            return False

        # Check if the node has configuration parameters that would make it run independently
        # (excluding standard parameters and None values)
        node_config = getattr(node_instance, "config", {})
        significant_config = {
            k: v
            for k, v in node_config.items()
            if k not in ["metadata", "name", "id"] and v is not None
        }

        # If the node has significant configuration, it might still be valuable to run
        if significant_config:
            # Check if any connected inputs have actual data (not None)
            connected_inputs = {}
            for _, _, edge_data in incoming_edges:
                mapping = edge_data.get("mapping", {})
                for source_key, target_key in mapping.items():
                    if target_key in inputs:
                        connected_inputs[target_key] = inputs[target_key]

            # If all connected inputs are None but node has config, still skip
            # The user can configure the node to run with default values if needed
            if all(v is None for v in connected_inputs.values()):
                return True

        # Check if all connected inputs are None
        # This is the main condition for conditional routing
        has_non_none_input = False

        # Count total connected inputs and None inputs from conditional sources
        total_connected_inputs = 0
        none_conditional_inputs = 0

        for source_node_id, _, edge_data in incoming_edges:
            mapping = edge_data.get("mapping", {})
            for source_key, target_key in mapping.items():
                if target_key in inputs:
                    total_connected_inputs += 1
                    if inputs[target_key] is not None:
                        has_non_none_input = True
                    else:
                        # Check if this None input came from conditional routing
                        source_node = workflow._node_instances.get(source_node_id)
                        is_from_conditional = (
                            source_node
                            and source_node.__class__.__name__ in ["SwitchNode"]
                        ) or (
                            hasattr(self, "_current_results")
                            and source_node_id in self._current_results
                            and self._current_results[source_node_id] is None
                        )
                        if is_from_conditional:
                            none_conditional_inputs += 1

        # Skip the node only if ALL connected inputs are None AND from conditional routing
        # This means nodes with mixed inputs (some None from conditional, some real data) should still execute
        if (
            total_connected_inputs > 0
            and none_conditional_inputs == total_connected_inputs
        ):
            return True

        return False

    def _should_stop_on_error(self, workflow: Workflow, node_id: str) -> bool:
        """Determine if execution should stop when a node fails.

        Args:
            workflow: The workflow being executed.
            node_id: Failed node ID.

        Returns:
            Whether to stop execution.
        """
        # Check if any downstream nodes depend on this node
        try:
            has_dependents = workflow.graph.out_degree(node_id) > 0
        except (TypeError, KeyError):
            # Handle case where node doesn't exist or graph issues
            has_dependents = False

        # For now, stop if the failed node has dependents
        # Future: implement configurable error handling policies
        return has_dependents

    def validate_workflow(self, workflow: Workflow) -> list[str]:
        """Validate a workflow before execution.

        Args:
            workflow: Workflow to validate

        Returns:
            List of validation warnings (empty if valid)

        Raises:
            WorkflowValidationError: If workflow is invalid
        """
        warnings = []

        try:
            workflow.validate()
        except WorkflowValidationError:
            # Re-raise validation errors
            raise
        except Exception as e:
            raise WorkflowValidationError(f"Workflow validation failed: {e}") from e

        # Check for disconnected nodes
        for node_id in workflow.graph.nodes():
            if (
                workflow.graph.in_degree(node_id) == 0
                and workflow.graph.out_degree(node_id) == 0
                and len(workflow.graph.nodes()) > 1
            ):
                warnings.append(f"Node '{node_id}' is disconnected from the workflow")

        # Check for missing required parameters
        for node_id, node_instance in workflow._node_instances.items():
            try:
                params = node_instance.get_parameters()
            except Exception as e:
                warnings.append(f"Failed to get parameters for node '{node_id}': {e}")
                continue

            for param_name, param_def in params.items():
                if param_def.required:
                    # Check if provided in config or connected
                    if param_name not in node_instance.config:
                        # Check if connected from another node
                        incoming_params = set()
                        for _, _, data in workflow.graph.in_edges(node_id, data=True):
                            mapping = data.get("mapping", {})
                            incoming_params.update(mapping.values())

                        if (
                            param_name not in incoming_params
                            and param_def.default is None
                        ):
                            warnings.append(
                                f"Node '{node_id}' missing required parameter '{param_name}' "
                                f"(no default value provided)"
                            )

        # Check for potential performance issues
        if len(workflow.graph.nodes()) > 100:
            warnings.append(
                f"Large workflow with {len(workflow.graph.nodes())} nodes "
                f"may have performance implications"
            )

        return warnings

    # Enterprise Feature Helper Methods

    def _check_workflow_access(self, workflow: Workflow) -> None:
        """Check if user has access to execute the workflow."""
        if not self.enable_security or not self.user_context:
            return

        try:
            # Use existing AccessControlManager pattern
            from kailash.access_control import (
                WorkflowPermission,
                get_access_control_manager,
            )

            if self._access_control_manager is None:
                self._access_control_manager = get_access_control_manager()

            decision = self._access_control_manager.check_workflow_access(
                self.user_context, workflow.workflow_id, WorkflowPermission.EXECUTE
            )
            if not decision.allowed:
                raise PermissionError(
                    f"Access denied to workflow '{workflow.workflow_id}': {decision.reason}"
                )
        except ImportError:
            # Access control not available, log and continue
            self.logger.warning(
                "Access control system not available, skipping security check"
            )
        except Exception as e:
            if isinstance(e, PermissionError):
                raise
            # Log but don't fail on access control errors
            self.logger.warning(f"Access control check failed: {e}")

    def _log_audit_event(self, event_type: str, event_data: dict[str, Any]) -> None:
        """Log audit events using enterprise audit logging (synchronous)."""
        if not self.enable_audit:
            return

        try:
            # Use existing AuditLogNode pattern
            from kailash.nodes.security.audit_log import AuditLogNode

            audit_node = AuditLogNode()
            # Use the SDK pattern - execute the node
            audit_node.execute(
                event_type=event_type,
                event_data=event_data,
                user_context=self.user_context,
                timestamp=datetime.now(UTC),
            )
        except ImportError:
            # Audit logging not available, fall back to standard logging
            self.logger.info(f"AUDIT: {event_type} - {event_data}")
        except Exception as e:
            # Audit logging failures shouldn't stop execution
            self.logger.warning(f"Audit logging failed: {e}")

    async def _log_audit_event_async(
        self, event_type: str, event_data: dict[str, Any]
    ) -> None:
        """Log audit events using enterprise audit logging (asynchronous)."""
        if not self.enable_audit:
            return

        try:
            # Use existing AuditLogNode pattern
            from kailash.nodes.security.audit_log import AuditLogNode

            audit_node = AuditLogNode()
            # Use the SDK pattern - try async first, fallback to sync
            if hasattr(audit_node, "async_run"):
                await audit_node.async_run(
                    event_type=event_type,
                    event_data=event_data,
                    user_context=self.user_context,
                    timestamp=datetime.now(UTC),
                )
            else:
                # Fallback to sync execution
                audit_node.execute(
                    event_type=event_type,
                    event_data=event_data,
                    user_context=self.user_context,
                    timestamp=datetime.now(UTC),
                )
        except ImportError:
            # Audit logging not available, fall back to standard logging
            self.logger.info(f"AUDIT: {event_type} - {event_data}")
        except Exception as e:
            # Audit logging failures shouldn't stop execution
            self.logger.warning(f"Audit logging failed: {e}")

    def _serialize_user_context(self) -> dict[str, Any] | None:
        """Serialize user context for logging/tracking."""
        if not self.user_context:
            return None

        try:
            # Try to use model_dump if it's a Pydantic model
            if hasattr(self.user_context, "model_dump"):
                return self.user_context.model_dump()
            # Try to use dict() if it's a Pydantic model
            elif hasattr(self.user_context, "dict"):
                return self.user_context.dict()
            # Convert to dict if possible
            elif hasattr(self.user_context, "__dict__"):
                return self.user_context.__dict__
            else:
                return {"user_context": str(self.user_context)}
        except Exception as e:
            self.logger.warning(f"Failed to serialize user context: {e}")
            return {"user_context": str(self.user_context)}

    def _process_workflow_parameters(
        self,
        workflow: Workflow,
        parameters: dict[str, dict[str, Any]] | dict[str, Any] | None = None,
    ) -> dict[str, dict[str, Any]] | None:
        """Process workflow parameters to handle both formats intelligently.

        This method detects whether parameters are in workflow-level format
        (flat dictionary) or node-specific format (nested dictionary) and
        transforms them appropriately for execution.

        ENTERPRISE ENHANCEMENT: Handles mixed format parameters where both
        node-specific and workflow-level parameters are present in the same
        parameter dictionary - critical for enterprise production workflows.

        Args:
            workflow: The workflow being executed
            parameters: Either workflow-level, node-specific, or MIXED format parameters

        Returns:
            Node-specific parameters ready for execution with workflow-level
            parameters properly injected
        """
        if not parameters:
            return None

        # ENTERPRISE FIX: Handle mixed format parameters
        # Extract node-specific and workflow-level parameters separately
        node_specific_params, workflow_level_params = self._separate_parameter_formats(
            parameters, workflow
        )

        # Start with node-specific parameters
        result = node_specific_params.copy() if node_specific_params else {}

        # If we have workflow-level parameters, inject them
        if workflow_level_params:
            injector = WorkflowParameterInjector(workflow, debug=self.debug)

            # Transform workflow parameters to node-specific format
            injected_params = injector.transform_workflow_parameters(
                workflow_level_params
            )

            # Merge injected parameters with existing node-specific parameters
            # IMPORTANT: Node-specific parameters take precedence over workflow-level
            for node_id, node_params in injected_params.items():
                if node_id not in result:
                    result[node_id] = {}
                # First set workflow-level parameters, then override with node-specific
                for param_name, param_value in node_params.items():
                    if param_name not in result[node_id]:  # Only if not already set
                        result[node_id][param_name] = param_value

            # Validate the transformation
            warnings = injector.validate_parameters(workflow_level_params)
            if warnings and self.debug:
                for warning in warnings:
                    self.logger.warning(f"Parameter validation: {warning}")

        # Inject secrets into the processed parameters
        if self.secret_provider:
            # Get secret requirements from workflow nodes
            requirements = self._extract_secret_requirements(workflow)
            if requirements:
                # Fetch secrets from provider
                secrets = self.secret_provider.get_secrets(requirements)

                # Inject secrets into workflow-level parameters
                if secrets:
                    # If we have workflow-level parameters, add secrets to them
                    if workflow_level_params:
                        workflow_level_params.update(secrets)

                        # Re-inject workflow parameters with secrets
                        injector = WorkflowParameterInjector(workflow, debug=self.debug)
                        injected_params = injector.transform_workflow_parameters(
                            workflow_level_params
                        )

                        # Merge secret-enhanced parameters
                        for node_id, node_params in injected_params.items():
                            if node_id not in result:
                                result[node_id] = {}
                            for param_name, param_value in node_params.items():
                                if param_name not in result[node_id]:
                                    result[node_id][param_name] = param_value
                    else:
                        # Create workflow-level parameters from secrets only
                        injector = WorkflowParameterInjector(workflow, debug=self.debug)
                        injected_params = injector.transform_workflow_parameters(
                            secrets
                        )

                        # Merge secret parameters
                        for node_id, node_params in injected_params.items():
                            if node_id not in result:
                                result[node_id] = {}
                            for param_name, param_value in node_params.items():
                                if param_name not in result[node_id]:
                                    result[node_id][param_name] = param_value

                    # Ensure result is not None if we added secrets
                    if result is None:
                        result = {}

        return result if result else None

    def _separate_parameter_formats(
        self, parameters: dict[str, Any], workflow: Workflow
    ) -> tuple[dict[str, dict[str, Any]], dict[str, Any]]:
        """Separate mixed format parameters into node-specific and workflow-level.

        ENTERPRISE CAPABILITY: Intelligently separates complex enterprise parameter
        patterns where both node-specific and workflow-level parameters coexist.

        Args:
            parameters: Mixed format parameters
            workflow: The workflow being executed

        Returns:
            Tuple of (node_specific_params, workflow_level_params)
        """
        node_specific_params = {}
        workflow_level_params = {}

        # Get node IDs for classification
        node_ids = set(workflow.graph.nodes()) if workflow else set()

        for key, value in parameters.items():
            # Node-specific parameter: key is a node ID and value is a dict
            if key in node_ids and isinstance(value, dict):
                node_specific_params[key] = value
            # Workflow-level parameter: key is not a node ID or value is not a dict
            else:
                workflow_level_params[key] = value

        if self.debug:
            self.logger.debug(
                f"Separated parameters: "
                f"node_specific={list(node_specific_params.keys())}, "
                f"workflow_level={list(workflow_level_params.keys())}"
            )

        return node_specific_params, workflow_level_params

    def _is_node_specific_format(
        self, parameters: dict[str, Any], workflow: Workflow = None
    ) -> bool:
        """Detect if parameters are in node-specific format.

        Node-specific format has structure: {node_id: {param: value}}
        Workflow-level format has structure: {param: value}

        Args:
            parameters: Parameters to check
            workflow: Optional workflow for node ID validation

        Returns:
            True if node-specific format, False if workflow-level
        """
        if not parameters:
            return True

        # Get node IDs if workflow provided
        node_ids = set(workflow.graph.nodes()) if workflow else set()

        # If any key is a node ID and its value is a dict, it's node-specific
        for key, value in parameters.items():
            if key in node_ids and isinstance(value, dict):
                return True

        # Additional heuristic: if all values are dicts and keys look like IDs
        all_dict_values = all(isinstance(v, dict) for v in parameters.values())
        keys_look_like_ids = any(
            "_" in k or k.startswith("node") or k in node_ids for k in parameters.keys()
        )

        if all_dict_values and keys_look_like_ids:
            return True

        # Default to workflow-level format
        return False

    def _validate_connection_contracts(
        self,
        workflow: Workflow,
        target_node_id: str,
        target_inputs: dict[str, Any],
        node_outputs: dict[str, dict[str, Any]],
    ) -> list[dict[str, str]]:
        """
        Validate connection contracts for a target node.

        Args:
            workflow: The workflow being executed
            target_node_id: ID of the target node
            target_inputs: Inputs being passed to the target node
            node_outputs: Outputs from all previously executed nodes

        Returns:
            List of contract violations (empty if all valid)
        """
        violations = []

        # Get connection contracts from workflow metadata
        connection_contracts = workflow.metadata.get("connection_contracts", {})
        if not connection_contracts:
            return violations  # No contracts to validate

        # Create contract validator
        validator = ContractValidator()

        # Find all connections targeting this node
        for connection in workflow.connections:
            if connection.target_node == target_node_id:
                connection_id = f"{connection.source_node}.{connection.source_output} → {connection.target_node}.{connection.target_input}"

                # Check if this connection has a contract
                if connection_id in connection_contracts:
                    contract_dict = connection_contracts[connection_id]

                    # Reconstruct contract from dictionary
                    contract = ConnectionContract.from_dict(contract_dict)

                    # Get source data from node outputs
                    source_data = None
                    if connection.source_node in node_outputs:
                        source_outputs = node_outputs[connection.source_node]
                        if connection.source_output in source_outputs:
                            source_data = source_outputs[connection.source_output]

                    # Get target data from inputs
                    target_data = target_inputs.get(connection.target_input)

                    # Validate the connection if we have data
                    if source_data is not None or target_data is not None:
                        is_valid, errors = validator.validate_connection(
                            contract, source_data, target_data
                        )

                        if not is_valid:
                            violations.append(
                                {
                                    "connection": connection_id,
                                    "contract": contract.name,
                                    "error": "; ".join(errors),
                                }
                            )

        return violations

    def _has_conditional_patterns(self, workflow: Workflow) -> bool:
        """
        Check if workflow has conditional patterns (SwitchNodes) and is suitable for conditional execution.

        CRITICAL: Only enable conditional execution for DAG workflows.
        Cyclic workflows must use normal execution to preserve cycle safety mechanisms.

        Args:
            workflow: Workflow to check

        Returns:
            True if workflow contains SwitchNode instances AND is a DAG (no cycles)
        """
        try:
            if not hasattr(workflow, "graph") or workflow.graph is None:
                return False

            # CRITICAL: Check for cycles first - conditional execution is only safe for DAGs
            if self._workflow_has_cycles(workflow):
                self.logger.info(
                    "Cyclic workflow detected - using normal execution to preserve cycle safety mechanisms"
                )
                return False

            # Import here to avoid circular dependencies
            from kailash.analysis import ConditionalBranchAnalyzer

            analyzer = ConditionalBranchAnalyzer(workflow)
            switch_nodes = analyzer._find_switch_nodes()

            has_switches = len(switch_nodes) > 0

            if has_switches:
                self.logger.debug(
                    f"Found {len(switch_nodes)} SwitchNodes in DAG workflow - eligible for conditional execution"
                )
            else:
                self.logger.debug("No SwitchNodes found - using normal execution")

            return has_switches

        except Exception as e:
            self.logger.warning(f"Error checking conditional patterns: {e}")
            return False

    def _workflow_has_cycles(self, workflow: Workflow) -> bool:
        """
        Detect if workflow has cycles using multiple detection methods.

        Args:
            workflow: Workflow to check

        Returns:
            True if workflow contains any cycles
        """
        try:
            # Method 1: Check for explicitly marked cycle connections
            if hasattr(workflow, "has_cycles") and callable(workflow.has_cycles):
                if workflow.has_cycles():
                    self.logger.debug("Detected cycles via workflow.has_cycles()")
                    return True

            # Method 2: Check for cycle edges in connections
            if hasattr(workflow, "connections"):
                for connection in workflow.connections:
                    if hasattr(connection, "cycle") and connection.cycle:
                        self.logger.debug("Detected cycle via connection.cycle flag")
                        return True

            # Method 3: NetworkX graph cycle detection
            if hasattr(workflow, "graph") and workflow.graph is not None:
                import networkx as nx

                is_dag = nx.is_directed_acyclic_graph(workflow.graph)
                if not is_dag:
                    self.logger.debug("Detected cycles via NetworkX graph analysis")
                    return True

            # Method 4: Check graph edges for cycle metadata
            if hasattr(workflow, "graph") and workflow.graph is not None:
                for u, v, edge_data in workflow.graph.edges(data=True):
                    if edge_data.get("cycle", False):
                        self.logger.debug("Detected cycle via edge metadata")
                        return True

            return False

        except Exception as e:
            self.logger.warning(f"Error detecting cycles: {e}")
            # On error, assume cycles exist for safety
            return True

    async def _execute_conditional_approach(
        self,
        workflow: Workflow,
        parameters: dict[str, Any],
        task_manager: TaskManager,
        run_id: str,
        workflow_context: dict[str, Any],
    ) -> dict[str, dict[str, Any]]:
        """
        Execute workflow using conditional approach with two-phase execution.

        Phase 1: Execute SwitchNodes to determine branches
        Phase 2: Execute only reachable nodes based on switch results

        Args:
            workflow: Workflow to execute
            parameters: Node-specific parameters
            task_manager: Task manager for execution
            run_id: Unique run identifier
            workflow_context: Workflow execution context

        Returns:
            Dictionary mapping node_id -> execution results
        """
        self.logger.info("Starting conditional execution approach")
        results = {}
        fallback_reason = None
        start_time = time.time()
        total_nodes = len(workflow.graph.nodes())

        try:
            # Enhanced pre-execution validation
            if not self._validate_conditional_execution_prerequisites(workflow):
                fallback_reason = "Prerequisites validation failed"
                raise ValueError(
                    f"Conditional execution prerequisites not met: {fallback_reason}"
                )

            # Phase 1: Execute SwitchNodes to determine conditional branches
            self.logger.info("Phase 1: Executing SwitchNodes")
            phase1_results = await self._execute_switch_nodes(
                workflow=workflow,
                parameters=parameters,
                task_manager=task_manager,
                run_id=run_id,
                workflow_context=workflow_context,
            )

            # Extract just switch results for validation and planning
            from kailash.analysis import ConditionalBranchAnalyzer

            analyzer = ConditionalBranchAnalyzer(workflow)
            switch_node_ids = analyzer._find_switch_nodes()
            switch_results = {
                node_id: phase1_results[node_id]
                for node_id in switch_node_ids
                if node_id in phase1_results
            }

            # Validate switch results before proceeding
            if not self._validate_switch_results(switch_results):
                fallback_reason = "Invalid switch results detected"
                raise ValueError(f"Switch results validation failed: {fallback_reason}")

            # Add all phase 1 results to overall results
            results.update(phase1_results)

            # Phase 2: Create pruned execution plan and execute remaining nodes
            self.logger.info("Phase 2: Creating and executing pruned plan")
            remaining_results = await self._execute_pruned_plan(
                workflow=workflow,
                switch_results=switch_results,
                parameters=parameters,
                task_manager=task_manager,
                run_id=run_id,
                workflow_context=workflow_context,
                existing_results=results,
            )

            # Merge remaining results
            results.update(remaining_results)

            # Final validation of conditional execution results
            if not self._validate_conditional_execution_results(results, workflow):
                fallback_reason = "Results validation failed"
                raise ValueError(
                    f"Conditional execution results invalid: {fallback_reason}"
                )

            # Performance tracking
            self._track_conditional_execution_performance(results, workflow)

            # Record execution metrics for performance monitoring
            execution_time = time.time() - start_time
            nodes_executed = len(results)
            nodes_skipped = total_nodes - nodes_executed

            self._record_execution_metrics(
                workflow=workflow,
                execution_time=execution_time,
                node_count=nodes_executed,
                skipped_nodes=nodes_skipped,
                execution_mode="skip_branches",
            )

            # Log performance improvement
            if nodes_skipped > 0:
                skip_percentage = (nodes_skipped / total_nodes) * 100
                self.logger.info(
                    f"Conditional execution performance: {skip_percentage:.1f}% reduction in executed nodes "
                    f"({nodes_skipped}/{total_nodes} skipped)"
                )

            self.logger.info(
                f"Conditional execution completed successfully: {nodes_executed} nodes executed"
            )
            return results

        except Exception as e:
            # Enhanced error logging with fallback reasoning
            self.logger.error(f"Error in conditional execution approach: {e}")
            if fallback_reason:
                self.logger.warning(f"Fallback reason: {fallback_reason}")

            # Log performance impact before fallback
            self._log_conditional_execution_failure(e, workflow, len(results))

            # Enhanced fallback with detailed logging
            self.logger.warning(
                "Falling back to normal execution approach due to conditional execution failure"
            )

            try:
                # Execute fallback with additional monitoring
                fallback_results, _ = await self._execute_async(
                    workflow=workflow,
                    parameters=parameters,
                    task_manager=task_manager,
                )

                # Track fallback usage for monitoring
                self._track_fallback_usage(workflow, str(e), fallback_reason)

                return fallback_results

            except Exception as fallback_error:
                self.logger.error(f"Fallback execution also failed: {fallback_error}")
                # If both conditional and fallback fail, re-raise the original error
                raise e from fallback_error

    async def _execute_switch_nodes(
        self,
        workflow: Workflow,
        parameters: dict[str, Any],
        task_manager: TaskManager,
        run_id: str,
        workflow_context: dict[str, Any],
    ) -> dict[str, dict[str, Any]]:
        """
        Execute SwitchNodes first to determine conditional branches.

        Args:
            workflow: Workflow being executed
            parameters: Node-specific parameters
            task_manager: Task manager for execution
            run_id: Unique run identifier
            workflow_context: Workflow execution context

        Returns:
            Dictionary mapping switch_node_id -> execution results
        """
        self.logger.info("Phase 1: Executing SwitchNodes and their dependencies")
        all_phase1_results = {}  # Store ALL results from Phase 1, not just switches

        try:
            # Import here to avoid circular dependencies
            from kailash.analysis import ConditionalBranchAnalyzer

            # Check if we should use hierarchical switch execution
            analyzer = ConditionalBranchAnalyzer(workflow)
            switch_node_ids = analyzer._find_switch_nodes()

            if switch_node_ids and self._should_use_hierarchical_execution(
                workflow, switch_node_ids
            ):
                # Use hierarchical switch executor for complex switch patterns
                self.logger.info(
                    "Using hierarchical switch execution for optimized performance"
                )
                from kailash.runtime.hierarchical_switch_executor import (
                    HierarchicalSwitchExecutor,
                )

                executor = HierarchicalSwitchExecutor(workflow, debug=self.debug)

                # Define node executor function
                async def node_executor(
                    node_id,
                    node_instance,
                    all_results,
                    parameters,
                    task_manager,
                    workflow,
                    workflow_context,
                ):
                    node_inputs = self._prepare_node_inputs(
                        workflow=workflow,
                        node_id=node_id,
                        node_instance=node_instance,
                        node_outputs=all_results,
                        parameters=parameters,
                    )

                    result = await self._execute_single_node(
                        node_id=node_id,
                        node_instance=node_instance,
                        node_inputs=node_inputs,
                        task_manager=task_manager,
                        workflow=workflow,
                        workflow_context=workflow_context,
                        run_id=run_id,
                    )
                    return result

                # Execute switches hierarchically
                all_results, switch_results = (
                    await executor.execute_switches_hierarchically(
                        parameters=parameters,
                        task_manager=task_manager,
                        run_id=run_id,
                        workflow_context=workflow_context,
                        node_executor=node_executor,
                    )
                )

                # Log execution summary
                if self.debug:
                    summary = executor.get_execution_summary(switch_results)
                    self.logger.debug(f"Hierarchical execution summary: {summary}")

                return all_results

            # Otherwise, use standard execution
            self.logger.info("Using standard switch execution")

            if not switch_node_ids:
                self.logger.info("No SwitchNodes found in workflow")
                return all_phase1_results

            # Get topological order for all nodes
            all_nodes_order = list(nx.topological_sort(workflow.graph))

            # Find all nodes that switches depend on (need to execute these too)
            nodes_to_execute = set(switch_node_ids)
            for switch_id in switch_node_ids:
                # Get all predecessors (direct and indirect) of this switch
                predecessors = nx.ancestors(workflow.graph, switch_id)
                nodes_to_execute.update(predecessors)

            # Execute nodes in topological order, but only those needed for switches
            execution_order = [
                node_id for node_id in all_nodes_order if node_id in nodes_to_execute
            ]

            self.logger.info(
                f"Executing {len(execution_order)} nodes in Phase 1 (switches and their dependencies)"
            )
            self.logger.debug(f"Phase 1 execution order: {execution_order}")

            # Execute all nodes needed for switches in dependency order
            for node_id in execution_order:
                try:
                    # Get node instance
                    node_data = workflow.graph.nodes[node_id]
                    # Try both 'node' and 'instance' keys for compatibility
                    node_instance = node_data.get("node") or node_data.get("instance")

                    if node_instance is None:
                        self.logger.warning(f"No instance found for node {node_id}")
                        continue

                    # Prepare inputs for the node
                    node_inputs = self._prepare_node_inputs(
                        workflow=workflow,
                        node_id=node_id,
                        node_instance=node_instance,
                        node_outputs=all_phase1_results,  # Use all results so far
                        parameters=parameters,
                    )

                    # CRITICAL FIX: During phase 1, ensure SwitchNodes don't get their 'value' parameter
                    # mistakenly used as 'input_data' when the actual input is missing
                    if not node_inputs or "input_data" not in node_inputs:
                        # Get incoming edges to check if input_data is expected
                        has_input_connection = False
                        for edge in workflow.graph.in_edges(switch_id, data=True):
                            mapping = edge[2].get("mapping", {})
                            if "input_data" in mapping.values():
                                has_input_connection = True
                                break

                        if has_input_connection:
                            # If input_data is expected from a connection but not available,
                            # explicitly set it to None to prevent config fallback
                            node_inputs["input_data"] = None

                    # Execute the switch
                    self.logger.debug(f"Executing SwitchNode: {switch_id}")
                    result = await self._execute_single_node(
                        node_id=node_id,
                        node_instance=node_instance,
                        node_inputs=node_inputs,
                        task_manager=task_manager,
                        workflow=workflow,
                        run_id=run_id,
                        workflow_context=workflow_context,
                    )

                    all_phase1_results[node_id] = result
                    self.logger.debug(
                        f"Node {node_id} completed with result keys: {list(result.keys()) if isinstance(result, dict) else type(result)}"
                    )

                except Exception as e:
                    self.logger.error(f"Error executing node {node_id}: {e}")
                    # Continue with other nodes
                    all_phase1_results[node_id] = {
                        "error": str(e),
                        "error_type": type(e).__name__,
                        "failed": True,
                    }

            # Extract just switch results to return
            switch_results = {
                node_id: all_phase1_results[node_id]
                for node_id in switch_node_ids
                if node_id in all_phase1_results
            }

            self.logger.info(
                f"Phase 1 completed: {len(all_phase1_results)} nodes executed ({len(switch_results)} switches)"
            )
            return all_phase1_results  # Return ALL results, not just switches

        except Exception as e:
            self.logger.error(f"Error in switch execution phase: {e}")
            return all_phase1_results

    async def _execute_pruned_plan(
        self,
        workflow: Workflow,
        switch_results: dict[str, dict[str, Any]],
        parameters: dict[str, Any],
        task_manager: TaskManager,
        run_id: str,
        workflow_context: dict[str, Any],
        existing_results: dict[str, dict[str, Any]],
    ) -> dict[str, dict[str, Any]]:
        """
        Execute pruned execution plan based on SwitchNode results.

        Args:
            workflow: Workflow being executed
            switch_results: Results from SwitchNode execution
            parameters: Node-specific parameters
            task_manager: Task manager for execution
            run_id: Unique run identifier
            workflow_context: Workflow execution context
            existing_results: Results from previous execution phases

        Returns:
            Dictionary mapping node_id -> execution results for remaining nodes
        """
        self.logger.info("Phase 2: Executing pruned plan based on switch results")
        remaining_results = {}

        try:
            # Import here to avoid circular dependencies
            from kailash.planning import DynamicExecutionPlanner

            planner = DynamicExecutionPlanner(workflow)

            # Create execution plan based on switch results
            execution_plan = planner.create_execution_plan(switch_results)
            self.logger.debug(
                f"DynamicExecutionPlanner returned plan: {execution_plan}"
            )

            # Remove nodes that were already executed, but check if switches need re-execution
            already_executed = set(existing_results.keys())
            self.logger.debug(
                f"Already executed nodes from Phase 1: {already_executed}"
            )
            self.logger.debug(f"Full execution plan for Phase 2: {execution_plan}")

            # Check which switches had incomplete execution (no input_data in phase 1)
            switches_needing_reexecution = set()
            for switch_id, result in switch_results.items():
                # If a switch executed with None input in phase 1, it needs re-execution
                if (
                    result.get("true_output") is None
                    and result.get("false_output") is None
                    and switch_id in execution_plan
                ):
                    # Check if this switch has dependencies that will now provide data
                    has_dependencies = False
                    for edge in workflow.graph.in_edges(switch_id):
                        source_node = edge[0]
                        if source_node in execution_plan:
                            has_dependencies = True
                            break

                    if has_dependencies:
                        switches_needing_reexecution.add(switch_id)
                        self.logger.debug(
                            f"Switch {switch_id} needs re-execution with actual data"
                        )

            # Include switches that need re-execution AND any nodes not yet executed
            remaining_nodes = [
                node_id
                for node_id in execution_plan
                if node_id not in already_executed
                or node_id in switches_needing_reexecution
            ]

            # Debug log to understand what's happening
            not_executed = set(execution_plan) - already_executed
            self.logger.debug(
                f"Nodes in execution plan but not executed: {not_executed}"
            )
            self.logger.debug(
                f"Switches needing re-execution: {switches_needing_reexecution}"
            )
            self.logger.debug(f"Filtering logic: remaining_nodes = {remaining_nodes}")

            self.logger.info(
                f"Executing {len(remaining_nodes)} remaining nodes after pruning"
            )
            self.logger.debug(f"Remaining execution plan: {remaining_nodes}")

            # Execute remaining nodes in the pruned order
            for node_id in remaining_nodes:
                try:
                    # Get node instance
                    node_data = workflow.graph.nodes[node_id]
                    # Try both 'node' and 'instance' keys for compatibility
                    node_instance = node_data.get("node") or node_data.get("instance")

                    if node_instance is None:
                        self.logger.warning(f"No instance found for node {node_id}")
                        continue

                    # Prepare inputs using all results so far (switches + remaining)
                    all_results = {**existing_results, **remaining_results}
                    node_inputs = self._prepare_node_inputs(
                        workflow=workflow,
                        node_id=node_id,
                        node_instance=node_instance,
                        node_outputs=all_results,
                        parameters=parameters,
                    )

                    # Execute the node
                    self.logger.debug(f"Executing remaining node: {node_id}")
                    result = await self._execute_single_node(
                        node_id=node_id,
                        node_instance=node_instance,
                        node_inputs=node_inputs,
                        task_manager=task_manager,
                        workflow=workflow,
                        run_id=run_id,
                        workflow_context=workflow_context,
                    )

                    remaining_results[node_id] = result
                    self.logger.debug(f"Node {node_id} completed")

                except Exception as e:
                    self.logger.error(f"Error executing remaining node {node_id}: {e}")
                    # Continue with other nodes or stop based on error handling
                    if self._should_stop_on_error(workflow, node_id):
                        raise
                    else:
                        remaining_results[node_id] = {
                            "error": str(e),
                            "error_type": type(e).__name__,
                            "failed": True,
                        }

            self.logger.info(
                f"Phase 2 completed: {len(remaining_results)} remaining nodes executed"
            )
            return remaining_results

        except Exception as e:
            self.logger.error(f"Error in pruned plan execution: {e}")
            return remaining_results

    async def _execute_single_node(
        self,
        node_id: str,
        node_instance: Any,
        node_inputs: dict[str, Any],
        task_manager: Any,
        workflow: Workflow,
        run_id: str,
        workflow_context: dict[str, Any],
    ) -> dict[str, Any]:
        """
        Execute a single node with proper validation and context setup.

        Args:
            node_id: Node identifier
            node_instance: Node instance to execute
            node_inputs: Prepared inputs for the node
            task_manager: Task manager for tracking
            workflow: Workflow being executed
            run_id: Unique run identifier
            workflow_context: Workflow execution context

        Returns:
            Node execution results
        """
        # Validate inputs before execution
        from kailash.utils.data_validation import DataTypeValidator

        validated_inputs = DataTypeValidator.validate_node_input(node_id, node_inputs)

        # Set workflow context on the node instance
        if hasattr(node_instance, "_workflow_context"):
            node_instance._workflow_context = workflow_context
        else:
            # Initialize the workflow context if it doesn't exist
            node_instance._workflow_context = workflow_context

        # Execute the node with unified async/sync support
        if self.enable_async and hasattr(node_instance, "execute_async"):
            # Use async execution method that includes validation
            outputs = await node_instance.execute_async(**validated_inputs)
        else:
            # Standard synchronous execution
            outputs = node_instance.execute(**validated_inputs)

        return outputs

    def _should_use_hierarchical_execution(
        self, workflow: Workflow, switch_node_ids: List[str]
    ) -> bool:
        """
        Determine if hierarchical switch execution should be used.

        Args:
            workflow: The workflow to analyze
            switch_node_ids: List of switch node IDs

        Returns:
            True if hierarchical execution would be beneficial
        """
        # Use hierarchical execution if:
        # 1. There are multiple switches
        if len(switch_node_ids) < 2:
            return False

        # 2. Check if switches have dependencies on each other
        from kailash.analysis import ConditionalBranchAnalyzer

        analyzer = ConditionalBranchAnalyzer(workflow)
        hierarchy_info = analyzer.analyze_switch_hierarchies(switch_node_ids)

        # Use hierarchical if there are multiple execution layers
        execution_layers = hierarchy_info.get("execution_layers", [])
        if len(execution_layers) > 1:
            self.logger.debug(
                f"Detected {len(execution_layers)} execution layers in switch hierarchy"
            )
            return True

        # Use hierarchical if there are dependency chains
        dependency_chains = hierarchy_info.get("dependency_chains", [])
        if dependency_chains and any(len(chain) > 1 for chain in dependency_chains):
            self.logger.debug("Detected dependency chains in switch hierarchy")
            return True

        return False

    def _validate_conditional_execution_prerequisites(self, workflow: Workflow) -> bool:
        """
        Validate that workflow meets prerequisites for conditional execution.

        Args:
            workflow: Workflow to validate

        Returns:
            True if prerequisites are met, False otherwise
        """
        try:
            # Check if workflow has at least one SwitchNode
            from kailash.analysis import ConditionalBranchAnalyzer

            analyzer = ConditionalBranchAnalyzer(workflow)
            switch_nodes = analyzer._find_switch_nodes()

            if not switch_nodes:
                self.logger.debug(
                    "No SwitchNodes found - cannot use conditional execution"
                )
                return False

            # Check if workflow is too complex for conditional execution
            if len(workflow.graph.nodes) > 100:  # Configurable threshold
                self.logger.warning(
                    "Workflow too large for conditional execution optimization"
                )
                return False

            # Validate that all SwitchNodes have proper outputs
            for switch_id in switch_nodes:
                node_data = workflow.graph.nodes[switch_id]
                node_instance = node_data.get("node") or node_data.get("instance")

                if node_instance is None:
                    self.logger.warning(f"SwitchNode {switch_id} has no instance")
                    return False

                # Check if the SwitchNode has proper output configuration
                # SwitchNode might store condition_field in different ways
                has_condition = (
                    hasattr(node_instance, "condition_field")
                    or hasattr(node_instance, "_condition_field")
                    or (
                        hasattr(node_instance, "parameters")
                        and "condition_field"
                        in getattr(node_instance, "parameters", {})
                    )
                    or "SwitchNode"
                    in str(type(node_instance))  # Type-based validation as fallback
                )

                if not has_condition:
                    self.logger.debug(
                        f"SwitchNode {switch_id} condition validation unclear - allowing execution"
                    )
                    # Don't fail here - let conditional execution attempt and fall back if needed

            return True

        except Exception as e:
            self.logger.warning(
                f"Error validating conditional execution prerequisites: {e}"
            )
            return False

    def _validate_switch_results(
        self, switch_results: dict[str, dict[str, Any]]
    ) -> bool:
        """
        Validate that switch results are valid for conditional execution.

        Args:
            switch_results: Results from SwitchNode execution

        Returns:
            True if results are valid, False otherwise
        """
        try:
            if not switch_results:
                self.logger.debug("No switch results to validate")
                return True

            for switch_id, result in switch_results.items():
                # Check for execution errors
                if isinstance(result, dict) and result.get("failed"):
                    self.logger.warning(
                        f"SwitchNode {switch_id} failed during execution"
                    )
                    return False

                # Validate result structure
                if not isinstance(result, dict):
                    self.logger.warning(
                        f"SwitchNode {switch_id} returned invalid result type: {type(result)}"
                    )
                    return False

                # Check for required output keys (at least one branch should be present)
                has_output = any(
                    key in result for key in ["true_output", "false_output"]
                )
                if not has_output:
                    self.logger.warning(
                        f"SwitchNode {switch_id} missing required output keys"
                    )
                    return False

            return True

        except Exception as e:
            self.logger.warning(f"Error validating switch results: {e}")
            return False

    def _validate_conditional_execution_results(
        self, results: dict[str, dict[str, Any]], workflow: Workflow
    ) -> bool:
        """
        Validate final results from conditional execution.

        Args:
            results: Execution results
            workflow: Original workflow

        Returns:
            True if results are valid, False otherwise
        """
        try:
            # Check that at least some nodes executed
            if not results:
                self.logger.warning("No results from conditional execution")
                return False

            # Validate that critical nodes (if any) were executed
            # This could be expanded based on workflow metadata
            total_nodes = len(workflow.graph.nodes)
            executed_nodes = len(results)

            # If we executed less than 30% of nodes, might be an issue
            if executed_nodes < (total_nodes * 0.3):
                self.logger.warning(
                    f"Conditional execution only ran {executed_nodes}/{total_nodes} nodes - might indicate an issue"
                )
                # Don't fail here, but log for monitoring

            # Check for excessive failures
            failed_nodes = sum(
                1
                for result in results.values()
                if isinstance(result, dict) and result.get("failed")
            )

            if failed_nodes > (executed_nodes * 0.5):
                self.logger.warning(
                    f"Too many node failures: {failed_nodes}/{executed_nodes}"
                )
                return False

            return True

        except Exception as e:
            self.logger.warning(f"Error validating conditional execution results: {e}")
            return False

    def _track_conditional_execution_performance(
        self, results: dict[str, dict[str, Any]], workflow: Workflow
    ):
        """
        Track performance metrics for conditional execution.

        Args:
            results: Execution results
            workflow: Original workflow
        """
        try:
            total_nodes = len(workflow.graph.nodes)
            executed_nodes = len(results)
            skipped_nodes = total_nodes - executed_nodes

            # Log performance metrics
            if skipped_nodes > 0:
                performance_improvement = (skipped_nodes / total_nodes) * 100
                self.logger.info(
                    f"Conditional execution performance: {performance_improvement:.1f}% reduction in executed nodes ({skipped_nodes}/{total_nodes} skipped)"
                )

            # Track for monitoring (could be sent to metrics system)
            if hasattr(self, "_performance_metrics"):
                self._performance_metrics["conditional_execution"] = {
                    "total_nodes": total_nodes,
                    "executed_nodes": executed_nodes,
                    "skipped_nodes": skipped_nodes,
                    "performance_improvement_percent": (
                        (skipped_nodes / total_nodes) * 100 if total_nodes > 0 else 0
                    ),
                }

        except Exception as e:
            self.logger.warning(
                f"Error tracking conditional execution performance: {e}"
            )

    def _log_conditional_execution_failure(
        self, error: Exception, workflow: Workflow, nodes_completed: int
    ):
        """
        Log detailed information about conditional execution failure.

        Args:
            error: Exception that caused the failure
            workflow: Workflow that failed
            nodes_completed: Number of nodes that completed before failure
        """
        try:
            total_nodes = len(workflow.graph.nodes)

            self.logger.error(
                f"Conditional execution failed after {nodes_completed}/{total_nodes} nodes"
            )
            self.logger.error(f"Error type: {type(error).__name__}")
            self.logger.error(f"Error message: {str(error)}")

            # Log workflow characteristics for debugging
            from kailash.analysis import ConditionalBranchAnalyzer

            analyzer = ConditionalBranchAnalyzer(workflow)
            switch_nodes = analyzer._find_switch_nodes()

            self.logger.debug(
                f"Workflow characteristics: {len(switch_nodes)} switches, {total_nodes} total nodes"
            )

        except Exception as log_error:
            self.logger.warning(
                f"Error logging conditional execution failure: {log_error}"
            )

    def _track_fallback_usage(
        self, workflow: Workflow, error_message: str, fallback_reason: str
    ):
        """
        Track fallback usage for monitoring and optimization.

        Args:
            workflow: Workflow that required fallback
            error_message: Error that triggered fallback
            fallback_reason: Reason for fallback
        """
        try:
            import time

            # Log fallback usage
            self.logger.info(
                f"Fallback used for workflow '{workflow.name}': {fallback_reason}"
            )

            # Track for monitoring (could be sent to metrics system)
            if hasattr(self, "_fallback_metrics"):
                if "fallback_usage" not in self._fallback_metrics:
                    self._fallback_metrics["fallback_usage"] = []

                self._fallback_metrics["fallback_usage"].append(
                    {
                        "workflow_name": workflow.name,
                        "workflow_id": workflow.workflow_id,
                        "error_message": error_message,
                        "fallback_reason": fallback_reason,
                        "timestamp": time.time(),
                    }
                )

            # Limit tracking history to prevent memory growth
            if (
                hasattr(self, "_fallback_metrics")
                and len(self._fallback_metrics.get("fallback_usage", [])) > 100
            ):
                self._fallback_metrics["fallback_usage"] = self._fallback_metrics[
                    "fallback_usage"
                ][-50:]

        except Exception as e:
            self.logger.warning(f"Error tracking fallback usage: {e}")

    # ===== PHASE 5: PRODUCTION READINESS =====

    def get_execution_plan_cached(
        self, workflow: Workflow, switch_results: Dict[str, Dict[str, Any]]
    ) -> List[str]:
        """
        Get execution plan with caching for improved performance.

        Args:
            workflow: Workflow to create execution plan for
            switch_results: Results from SwitchNode execution

        Returns:
            Cached or newly computed execution plan
        """
        # Create cache key based on workflow structure and switch results
        cache_key = self._create_execution_plan_cache_key(workflow, switch_results)

        if cache_key in self._execution_plan_cache:
            self._analytics_data["cache_hits"] += 1
            self.logger.debug(f"Cache hit for execution plan: {cache_key[:32]}...")
            return self._execution_plan_cache[cache_key]

        # Cache miss - compute new plan
        self._analytics_data["cache_misses"] += 1
        self.logger.debug(f"Cache miss for execution plan: {cache_key[:32]}...")

        try:
            from kailash.planning import DynamicExecutionPlanner

            planner = DynamicExecutionPlanner(workflow)
            execution_plan = planner.create_execution_plan(switch_results)

            # Cache the result (with size limit)
            if len(self._execution_plan_cache) >= 100:  # Limit cache size
                # Remove oldest entries (simple FIFO)
                oldest_key = next(iter(self._execution_plan_cache))
                del self._execution_plan_cache[oldest_key]

            self._execution_plan_cache[cache_key] = execution_plan

        except Exception as e:
            self.logger.warning(f"Error creating cached execution plan: {e}")
            # Fallback to basic topological order
            execution_plan = list(nx.topological_sort(workflow.graph))

        return execution_plan

    def _create_execution_plan_cache_key(
        self, workflow: Workflow, switch_results: Dict[str, Dict[str, Any]]
    ) -> str:
        """
        Create cache key for execution plan.

        Args:
            workflow: Workflow instance
            switch_results: SwitchNode results

        Returns:
            Cache key string
        """
        import json

        try:
            # Create key from workflow structure + switch results
            workflow_key = f"{workflow.workflow_id}_{len(workflow.graph.nodes)}_{len(workflow.graph.edges)}"

            # Sort switch results for consistent caching
            sorted_results = {}
            for switch_id, result in switch_results.items():
                if isinstance(result, dict):
                    # Create deterministic representation
                    sorted_results[switch_id] = {
                        k: v
                        for k, v in sorted(result.items())
                        if k in ["true_output", "false_output", "condition_result"]
                    }

            results_str = json.dumps(sorted_results, sort_keys=True, default=str)
            combined_key = f"{workflow_key}:{results_str}"

            # Hash to fixed length
            return hashlib.md5(combined_key.encode()).hexdigest()

        except Exception as e:
            self.logger.warning(f"Error creating cache key: {e}")
            # Fallback to simple key
            return f"{workflow.workflow_id}_{hash(str(switch_results))}"

    def get_execution_analytics(self) -> Dict[str, Any]:
        """
        Get comprehensive execution analytics for monitoring and optimization.

        Returns:
            Dictionary containing detailed analytics data
        """
        analytics = {
            "cache_performance": {
                "hits": self._analytics_data["cache_hits"],
                "misses": self._analytics_data["cache_misses"],
                "hit_rate": self._analytics_data["cache_hits"]
                / max(
                    1,
                    self._analytics_data["cache_hits"]
                    + self._analytics_data["cache_misses"],
                ),
            },
            "conditional_execution_stats": {
                "total_executions": len(self._analytics_data["conditional_executions"]),
                "average_performance_improvement": 0.0,
                "fallback_rate": 0.0,
            },
            "performance_history": self._analytics_data["performance_history"][
                -50:
            ],  # Last 50 executions
            "execution_patterns": self._analytics_data["execution_patterns"],
            "optimization_stats": self._analytics_data["optimization_stats"],
        }

        # Calculate conditional execution statistics
        if self._analytics_data["conditional_executions"]:
            improvements = [
                exec_data.get("performance_improvement", 0)
                for exec_data in self._analytics_data["conditional_executions"]
            ]
            analytics["conditional_execution_stats"][
                "average_performance_improvement"
            ] = sum(improvements) / len(improvements)

            fallbacks = sum(
                1
                for exec_data in self._analytics_data["conditional_executions"]
                if exec_data.get("used_fallback", False)
            )
            analytics["conditional_execution_stats"]["fallback_rate"] = fallbacks / len(
                self._analytics_data["conditional_executions"]
            )

        # Add cache statistics
        cache_size = len(self._execution_plan_cache)
        analytics["cache_performance"]["cache_size"] = cache_size
        analytics["cache_performance"]["cache_efficiency"] = min(
            1.0, cache_size / 100.0
        )  # Relative to max size

        return analytics

    def record_execution_performance(
        self,
        workflow: Workflow,
        execution_time: float,
        nodes_executed: int,
        used_conditional: bool,
        performance_improvement: float = 0.0,
    ):
        """
        Record execution performance for analytics.

        Args:
            workflow: Workflow that was executed
            execution_time: Total execution time in seconds
            nodes_executed: Number of nodes actually executed
            used_conditional: Whether conditional execution was used
            performance_improvement: Performance improvement percentage (0.0-1.0)
        """
        import time

        performance_record = {
            "timestamp": time.time(),
            "workflow_id": workflow.workflow_id,
            "workflow_name": workflow.name,
            "total_nodes": len(workflow.graph.nodes),
            "executed_nodes": nodes_executed,
            "execution_time": execution_time,
            "used_conditional_execution": used_conditional,
            "performance_improvement": performance_improvement,
            "nodes_per_second": nodes_executed / max(0.001, execution_time),
        }

        # Add to performance history
        self._analytics_data["performance_history"].append(performance_record)

        # Limit history size
        if len(self._analytics_data["performance_history"]) > 1000:
            self._analytics_data["performance_history"] = self._analytics_data[
                "performance_history"
            ][-500:]

        # Record conditional execution if used
        if used_conditional:
            self._analytics_data["conditional_executions"].append(
                {
                    "timestamp": time.time(),
                    "workflow_id": workflow.workflow_id,
                    "performance_improvement": performance_improvement,
                    "nodes_skipped": len(workflow.graph.nodes) - nodes_executed,
                    "used_fallback": False,  # Set by fallback tracking
                }
            )

        # Update execution patterns
        pattern_key = f"{len(workflow.graph.nodes)}_nodes"
        if pattern_key not in self._analytics_data["execution_patterns"]:
            self._analytics_data["execution_patterns"][pattern_key] = {
                "count": 0,
                "avg_execution_time": 0.0,
                "avg_performance_improvement": 0.0,
            }

        pattern = self._analytics_data["execution_patterns"][pattern_key]
        pattern["count"] += 1
        pattern["avg_execution_time"] = (
            pattern["avg_execution_time"] * (pattern["count"] - 1) + execution_time
        ) / pattern["count"]
        if used_conditional:
            pattern["avg_performance_improvement"] = (
                pattern["avg_performance_improvement"] * (pattern["count"] - 1)
                + performance_improvement
            ) / pattern["count"]

    def clear_analytics_data(self, keep_patterns: bool = True):
        """
        Clear analytics data for fresh monitoring.

        Args:
            keep_patterns: Whether to preserve execution patterns
        """
        self._analytics_data["conditional_executions"] = []
        self._analytics_data["performance_history"] = []
        self._analytics_data["cache_hits"] = 0
        self._analytics_data["cache_misses"] = 0

        if not keep_patterns:
            self._analytics_data["execution_patterns"] = {}
            self._analytics_data["optimization_stats"] = {}

        # Clear caches
        self._execution_plan_cache.clear()

        self.logger.info("Analytics data cleared")

    def get_health_diagnostics(self) -> Dict[str, Any]:
        """
        Get health diagnostics for monitoring system health.

        Returns:
            Dictionary containing health check results
        """
        import os
        import time

        diagnostics = {
            "timestamp": time.time(),
            "runtime_health": "healthy",
            "cache_health": "healthy",
            "performance_health": "healthy",
            "memory_usage": {},
            "cache_statistics": {},
            "performance_indicators": {},
            "warnings": [],
            "errors": [],
        }

        try:
            # Memory usage
            process = psutil.Process(os.getpid())
            memory_info = process.memory_info()
            diagnostics["memory_usage"] = {
                "rss_mb": memory_info.rss / 1024 / 1024,
                "vms_mb": memory_info.vms / 1024 / 1024,
                "percent": process.memory_percent(),
            }

            # Cache health
            cache_size = len(self._execution_plan_cache)
            analytics = self.get_execution_analytics()
            cache_hit_rate = analytics["cache_performance"]["hit_rate"]

            diagnostics["cache_statistics"] = {
                "size": cache_size,
                "hit_rate": cache_hit_rate,
                "hits": analytics["cache_performance"]["hits"],
                "misses": analytics["cache_performance"]["misses"],
            }

            # Performance indicators
            recent_executions = self._analytics_data["performance_history"][-10:]
            if recent_executions:
                avg_execution_time = sum(
                    e["execution_time"] for e in recent_executions
                ) / len(recent_executions)
                avg_improvement = sum(
                    e["performance_improvement"] for e in recent_executions
                ) / len(recent_executions)

                diagnostics["performance_indicators"] = {
                    "avg_execution_time": avg_execution_time,
                    "avg_performance_improvement": avg_improvement,
                    "recent_executions": len(recent_executions),
                }

            # Health checks
            if (
                cache_hit_rate < 0.3
                and analytics["cache_performance"]["hits"]
                + analytics["cache_performance"]["misses"]
                > 10
            ):
                diagnostics["warnings"].append(
                    "Low cache hit rate - consider workflow optimization"
                )
                diagnostics["cache_health"] = "warning"

            if diagnostics["memory_usage"]["percent"] > 80:
                diagnostics["warnings"].append("High memory usage detected")
                diagnostics["runtime_health"] = "warning"

            if recent_executions and avg_execution_time > 5.0:
                diagnostics["warnings"].append("Slow execution times detected")
                diagnostics["performance_health"] = "warning"

        except Exception as e:
            diagnostics["errors"].append(f"Health check error: {e}")
            diagnostics["runtime_health"] = "error"

        return diagnostics

    def optimize_runtime_performance(self) -> Dict[str, Any]:
        """
        Optimize runtime performance based on analytics data.

        Returns:
            Dictionary describing optimizations applied
        """
        optimization_result = {
            "optimizations_applied": [],
            "performance_impact": {},
            "recommendations": [],
            "cache_optimizations": {},
            "memory_optimizations": {},
        }

        try:
            # Cache optimization
            cache_analytics = self.get_execution_analytics()["cache_performance"]

            if (
                cache_analytics["hit_rate"] < 0.5
                and cache_analytics["hits"] + cache_analytics["misses"] > 20
            ):
                # Poor cache performance - clear and rebuild
                old_size = len(self._execution_plan_cache)
                self._execution_plan_cache.clear()
                optimization_result["optimizations_applied"].append("cache_clear")
                optimization_result["cache_optimizations"]["cleared_entries"] = old_size
                optimization_result["recommendations"].append(
                    "Consider using more consistent workflows for better caching"
                )

            # Memory optimization
            if len(self._analytics_data["performance_history"]) > 500:
                old_count = len(self._analytics_data["performance_history"])
                self._analytics_data["performance_history"] = self._analytics_data[
                    "performance_history"
                ][-250:]
                optimization_result["optimizations_applied"].append("history_cleanup")
                optimization_result["memory_optimizations"][
                    "history_entries_removed"
                ] = (old_count - 250)

            # Execution pattern analysis
            patterns = self._analytics_data["execution_patterns"]
            if patterns:
                most_common_pattern = max(patterns.items(), key=lambda x: x[1]["count"])
                optimization_result["recommendations"].append(
                    f"Most common pattern: {most_common_pattern[0]} with {most_common_pattern[1]['count']} executions"
                )

                # Suggest optimizations based on patterns
                for pattern_key, pattern_data in patterns.items():
                    if pattern_data["avg_execution_time"] > 3.0:
                        optimization_result["recommendations"].append(
                            f"Consider optimizing workflows with {pattern_key} - avg time: {pattern_data['avg_execution_time']:.2f}s"
                        )

            self.logger.info(
                f"Runtime optimization completed: {len(optimization_result['optimizations_applied'])} optimizations applied"
            )

        except Exception as e:
            self.logger.warning(f"Error during runtime optimization: {e}")
            optimization_result["error"] = str(e)

        return optimization_result

    # ===== PHASE 3 COMPLETION: Performance Monitoring & Compatibility =====

    def _check_performance_switch(self, current_mode: str) -> Tuple[bool, str, str]:
        """Check if execution mode should be switched based on performance.

        Args:
            current_mode: Current execution mode

        Returns:
            Tuple of (should_switch, recommended_mode, reason)
        """
        # Initialize performance monitor if needed
        if self._performance_monitor is None:
            self._performance_monitor = PerformanceMonitor()

        return self._performance_monitor.should_switch_mode(current_mode)

    def _record_execution_metrics(
        self,
        workflow: Workflow,
        execution_time: float,
        node_count: int,
        skipped_nodes: int,
        execution_mode: str,
    ) -> None:
        """Record execution metrics for performance monitoring.

        Args:
            workflow: Executed workflow
            execution_time: Total execution time
            node_count: Number of nodes executed
            skipped_nodes: Number of nodes skipped
            execution_mode: Execution mode used
        """
        if not self._enable_performance_monitoring:
            return

        # Initialize performance monitor if needed
        if self._performance_monitor is None:
            self._performance_monitor = PerformanceMonitor()

        metrics = ExecutionMetrics(
            execution_time=execution_time,
            node_count=node_count,
            skipped_nodes=skipped_nodes,
            execution_mode=execution_mode,
        )

        self._performance_monitor.record_execution(metrics)

    def get_performance_report(self) -> Dict[str, Any]:
        """Get performance monitoring report.

        Returns:
            Performance statistics and recommendations
        """
        if self._performance_monitor is None:
            return {"status": "Performance monitoring not initialized"}

        return self._performance_monitor.get_performance_report()

    def generate_compatibility_report(self, workflow: Workflow) -> Dict[str, Any]:
        """Generate compatibility report for a workflow.

        Args:
            workflow: Workflow to analyze

        Returns:
            Compatibility report dictionary
        """
        if not self._enable_compatibility_reporting:
            return {"status": "Compatibility reporting disabled"}

        # Initialize reporter if needed
        if self._compatibility_reporter is None:
            self._compatibility_reporter = CompatibilityReporter()

        report = self._compatibility_reporter.analyze_workflow(workflow)
        return report.to_dict()

    def get_compatibility_report_markdown(self, workflow: Workflow) -> str:
        """Generate compatibility report in markdown format.

        Args:
            workflow: Workflow to analyze

        Returns:
            Markdown formatted report
        """
        if not self._enable_compatibility_reporting:
            return "# Compatibility reporting disabled"

        # Initialize reporter if needed
        if self._compatibility_reporter is None:
            self._compatibility_reporter = CompatibilityReporter()

        report = self._compatibility_reporter.analyze_workflow(workflow)
        return report.to_markdown()

    def set_performance_monitoring(self, enabled: bool) -> None:
        """Enable or disable performance monitoring.

        Args:
            enabled: Whether to enable performance monitoring
        """
        self._enable_performance_monitoring = enabled
        self.logger.info(
            f"Performance monitoring {'enabled' if enabled else 'disabled'}"
        )

    def set_automatic_mode_switching(self, enabled: bool) -> None:
        """Enable or disable automatic mode switching based on performance.

        Args:
            enabled: Whether to enable automatic switching
        """
        self._performance_switch_enabled = enabled
        self.logger.info(
            f"Automatic mode switching {'enabled' if enabled else 'disabled'}"
        )

    def set_compatibility_reporting(self, enabled: bool) -> None:
        """Enable or disable compatibility reporting.

        Args:
            enabled: Whether to enable compatibility reporting
        """
        self._enable_compatibility_reporting = enabled
        self.logger.info(
            f"Compatibility reporting {'enabled' if enabled else 'disabled'}"
        )

    def get_execution_path_debug_info(self) -> Dict[str, Any]:
        """Get detailed debug information about execution paths.

        Returns:
            Debug information including execution decisions and paths
        """
        debug_info = {
            "conditional_execution_mode": self.conditional_execution,
            "performance_monitoring_enabled": self._enable_performance_monitoring,
            "automatic_switching_enabled": self._performance_switch_enabled,
            "compatibility_reporting_enabled": self._enable_compatibility_reporting,
            "fallback_metrics": self._fallback_metrics,
            "execution_analytics": self.get_execution_analytics(),
        }

        if self._performance_monitor:
            debug_info["performance_report"] = self.get_performance_report()

        return debug_info
