"""Workflow builder implementation for the Kailash SDK."""

import logging
import uuid
from typing import TYPE_CHECKING, Any

from kailash.nodes.base import Node
from kailash.sdk_exceptions import ConnectionError, WorkflowValidationError
from kailash.workflow.graph import Workflow

logger = logging.getLogger(__name__)


class WorkflowBuilder:
    """Builder pattern for creating Workflow instances."""

    def __init__(self):
        """Initialize an empty workflow builder."""
        self.nodes: dict[str, dict[str, Any]] = {}
        self.connections: list[dict[str, str]] = []
        self._metadata: dict[str, Any] = {}
        # Parameter injection capabilities
        self.workflow_parameters: dict[str, Any] = {}
        self.parameter_mappings: dict[str, dict[str, str]] = {}

    def add_node(self, *args, **kwargs) -> str:
        """
        Unified add_node method supporting multiple API patterns.

        Supported patterns:
        1. add_node("NodeType", "node_id", {"param": value})     # Current/Preferred
        2. add_node("node_id", NodeClass, param=value)           # Legacy fluent
        3. add_node(NodeClass, "node_id", param=value)           # Alternative

        Args:
            *args: Positional arguments (pattern-dependent)
            **kwargs: Keyword arguments for configuration

        Returns:
            Node ID (useful for method chaining)

        Raises:
            WorkflowValidationError: If node_id is already used or invalid pattern
        """
        # Pattern detection and routing
        if len(args) == 0 and kwargs:
            # Keyword-only pattern: add_node(node_type="NodeType", node_id="id", config={})
            node_type = kwargs.pop("node_type", None)
            node_id = kwargs.pop("node_id", None)
            config = kwargs.pop("config", {})
            # Any remaining kwargs are treated as config
            config.update(kwargs)

            if node_type is None:
                raise WorkflowValidationError(
                    "node_type is required when using keyword arguments"
                )

            return self._add_node_current(node_type, node_id, config)

        elif len(args) == 1:
            # Single argument with possible keywords
            if isinstance(args[0], str) and kwargs:
                # Pattern: add_node("NodeType", node_id="id", config={})
                node_type = args[0]
                node_id = kwargs.pop("node_id", None)
                config = kwargs.pop("config", {})
                # Any remaining kwargs are treated as config
                config.update(kwargs)
                return self._add_node_current(node_type, node_id, config)
            elif isinstance(args[0], str):
                # Pattern: add_node("NodeType")
                return self._add_node_current(args[0], None, {})
            elif hasattr(args[0], "__name__"):
                # Pattern: add_node(NodeClass)
                return self._add_node_alternative(args[0], None, **kwargs)
            else:
                if isinstance(args[0], Node):
                    # Pattern: add_node(node_instance)
                    return self._add_node_instance(args[0], None)

        elif len(args) == 3 and isinstance(args[0], str) and isinstance(args[2], dict):
            # Pattern 1: Current API - add_node("NodeType", "node_id", {"param": value})
            return self._add_node_current(args[0], args[1], args[2])

        elif len(args) >= 2 and isinstance(args[0], str):
            # Pattern 2: Legacy fluent API - add_node("node_id", NodeClass, param=value)
            if hasattr(args[1], "__name__") or isinstance(args[1], type):
                return self._add_node_legacy_fluent(args[0], args[1], **kwargs)
            elif isinstance(args[1], str):
                # Two strings - assume current API: add_node("NodeType", "node_id")
                config = kwargs if kwargs else (args[2] if len(args) > 2 else {})
                return self._add_node_current(args[0], args[1], config)
            else:
                # Invalid second argument
                raise WorkflowValidationError(
                    f"Invalid node type: {type(args[1]).__name__}. "
                    "Expected: str (node type name), Node class, or Node instance"
                )

        elif len(args) >= 2 and hasattr(args[0], "__name__"):
            # Pattern 3: Alternative - add_node(NodeClass, "node_id", param=value)
            # Handle both dict config and keyword args
            if len(args) == 3 and isinstance(args[2], dict):
                # Config provided as dict
                return self._add_node_alternative(args[0], args[1], **args[2])
            else:
                # Config provided as kwargs
                return self._add_node_alternative(args[0], args[1], **kwargs)

        elif len(args) >= 2:
            # Check if first arg is a Node instance
            if isinstance(args[0], Node):
                # Pattern 4: Instance - add_node(node_instance, "node_id") or add_node(node_instance, "node_id", config)
                # Config is ignored for instances
                return self._add_node_instance(args[0], args[1])
            elif len(args) == 2:
                # Invalid arguments for 2-arg call
                raise WorkflowValidationError(
                    f"Invalid node type: {type(args[0]).__name__}. "
                    "Expected: str (node type name), Node class, or Node instance"
                )

            # For 3 or more args that don't match other patterns
            # Error with helpful message
            raise WorkflowValidationError(
                f"Invalid add_node signature. Received {len(args)} args: {[type(arg).__name__ for arg in args]}\n"
                f"Supported patterns:\n"
                f"  add_node('NodeType', 'node_id', {{'param': value}})  # Preferred\n"
                f"  add_node('node_id', NodeClass, param=value)          # Legacy\n"
                f"  add_node(NodeClass, 'node_id', param=value)          # Alternative\n"
                f"Examples:\n"
                f"  add_node('HTTPRequestNode', 'api_call', {{'url': 'https://api.com'}})\n"
                f"  add_node('csv_reader', CSVReaderNode, file_path='data.csv')"
            )

    def _add_node_current(
        self, node_type: str, node_id: str | None, config: dict[str, Any]
    ) -> str:
        """Handle current API pattern: add_node('NodeType', 'node_id', {'param': value})"""
        return self._add_node_unified(node_type, node_id, config)

    def _add_node_legacy_fluent(
        self, node_id: str, node_class_or_type: Any, **config
    ) -> "WorkflowBuilder":
        """Handle legacy fluent API pattern: add_node('node_id', NodeClass, param=value)"""
        import warnings

        # If it's a class, validate it's a Node subclass
        if isinstance(node_class_or_type, type) and not issubclass(
            node_class_or_type, Node
        ):
            raise WorkflowValidationError(
                f"Invalid node type: {node_class_or_type}. Expected a Node subclass or string."
            )

        warnings.warn(
            f"Legacy fluent API usage detected. "
            f"Migration guide:\n"
            f"  OLD: add_node('{node_id}', {getattr(node_class_or_type, '__name__', str(node_class_or_type))}, {list(config.keys())})\n"
            f"  NEW: add_node('{getattr(node_class_or_type, '__name__', str(node_class_or_type))}', '{node_id}', {config})\n"
            f"Legacy support will be removed in v0.8.0",
            DeprecationWarning,
            stacklevel=3,
        )

        if hasattr(node_class_or_type, "__name__"):
            node_type = node_class_or_type.__name__
        else:
            node_type = str(node_class_or_type)

        self._add_node_unified(node_type, node_id, config)
        return self  # Return self for fluent chaining

    def _add_node_alternative(
        self, node_class: type, node_id: str | None, **config
    ) -> str:
        """Handle alternative pattern: add_node(NodeClass, 'node_id', param=value)"""
        import warnings

        # Validate that node_class is actually a Node subclass
        if not isinstance(node_class, type) or not issubclass(node_class, Node):
            raise WorkflowValidationError(
                f"Invalid node type: {node_class}. Expected a Node subclass."
            )

        # Generate ID if not provided
        if node_id is None:
            node_id = f"node_{uuid.uuid4().hex[:8]}"

        warnings.warn(
            f"Alternative API usage detected. Consider using preferred pattern:\n"
            f"  CURRENT: add_node({node_class.__name__}, '{node_id}', {list(config.keys())})\n"
            f"  PREFERRED: add_node('{node_class.__name__}', '{node_id}', {config})",
            UserWarning,
            stacklevel=3,
        )

        # Store the class reference along with the type name
        self.nodes[node_id] = {
            "type": node_class.__name__,
            "config": config,
            "class": node_class,
        }
        logger.info(f"Added node '{node_id}' of type '{node_class.__name__}'")
        return node_id

    def _add_node_instance(self, node_instance: "Node", node_id: str | None) -> str:
        """Handle instance pattern: add_node(node_instance, 'node_id')"""
        import warnings

        # Generate ID if not provided
        if node_id is None:
            node_id = f"node_{uuid.uuid4().hex[:8]}"

        warnings.warn(
            f"Instance-based API usage detected. Consider using preferred pattern:\n"
            f"  CURRENT: add_node(<instance>, '{node_id}')\n"
            f"  PREFERRED: add_node('{node_instance.__class__.__name__}', '{node_id}', {{'param': value}})",
            UserWarning,
            stacklevel=3,
        )

        # Store the instance
        self.nodes[node_id] = {
            "instance": node_instance,
            "type": node_instance.__class__.__name__,
        }
        logger.info(
            f"Added node '{node_id}' with instance of type '{node_instance.__class__.__name__}'"
        )
        return node_id

    def _add_node_unified(
        self,
        node_type: str,
        node_id: str | None = None,
        config: dict[str, Any] | None = None,
    ) -> str:
        """
        Unified implementation for all add_node patterns.

        Args:
            node_type: Node type name (string)
            node_id: Unique identifier for this node (auto-generated if not provided)
            config: Configuration for the node

        Returns:
            Node ID

        Raises:
            WorkflowValidationError: If node_id is already used
        """
        # Generate ID if not provided
        if node_id is None:
            node_id = f"node_{uuid.uuid4().hex[:8]}"

        if node_id in self.nodes:
            raise WorkflowValidationError(
                f"Node ID '{node_id}' already exists in workflow"
            )

        # Handle different input types
        if isinstance(node_type, str):
            # String node type name
            self.nodes[node_id] = {"type": node_type, "config": config or {}}
            type_name = node_type
        elif isinstance(node_type, type) and issubclass(node_type, Node):
            # Node class
            self.nodes[node_id] = {
                "type": node_type.__name__,
                "config": config or {},
                "class": node_type,
            }
            type_name = node_type.__name__
        elif hasattr(node_type, "__class__") and issubclass(node_type.__class__, Node):
            # Node instance
            self.nodes[node_id] = {
                "instance": node_type,
                "type": node_type.__class__.__name__,
            }
            type_name = node_type.__class__.__name__
        else:
            raise WorkflowValidationError(
                f"Invalid node type: {type(node_type)}. "
                "Expected: str (node type name), Node class, or Node instance"
            )

        logger.info(f"Added node '{node_id}' of type '{type_name}'")
        return node_id

    # Fluent API methods for backward compatibility
    def add_node_fluent(
        self, node_id: str, node_class_or_type: Any, **config
    ) -> "WorkflowBuilder":
        """
        DEPRECATED: Fluent API for backward compatibility.
        Use add_node(node_type, node_id, config) instead.

        Args:
            node_id: Node identifier
            node_class_or_type: Node class or type
            **config: Node configuration as keyword arguments

        Returns:
            Self for method chaining
        """
        import warnings

        warnings.warn(
            "Fluent API is deprecated. Use add_node(node_type, node_id, config) instead.",
            DeprecationWarning,
            stacklevel=2,
        )

        if hasattr(node_class_or_type, "__name__"):
            # Node class
            self.add_node(node_class_or_type.__name__, node_id, config)
        else:
            # Assume string type
            self.add_node(str(node_class_or_type), node_id, config)

        return self

    def add_node_instance(self, node_instance: Any, node_id: str | None = None) -> str:
        """
        Add a node instance to the workflow.

        This is a convenience method for adding pre-configured node instances.

        Args:
            node_instance: Pre-configured node instance
            node_id: Unique identifier for this node (auto-generated if not provided)

        Returns:
            Node ID

        Raises:
            WorkflowValidationError: If node_id is already used or instance is invalid
        """
        return self.add_node(node_instance, node_id)

    def add_node_type(
        self,
        node_type: str,
        node_id: str | None = None,
        config: dict[str, Any] | None = None,
    ) -> str:
        """
        Add a node by type name to the workflow.

        This is the original string-based method, provided for clarity and backward compatibility.

        Args:
            node_type: Node type name as string
            node_id: Unique identifier for this node (auto-generated if not provided)
            config: Configuration for the node

        Returns:
            Node ID

        Raises:
            WorkflowValidationError: If node_id is already used
        """
        return self.add_node(node_type, node_id, config)

    def add_connection(
        self, from_node: str, from_output: str, to_node: str, to_input: str
    ) -> "WorkflowBuilder":
        """
        Connect two nodes in the workflow.

        Args:
            from_node: Source node ID
            from_output: Output field from source
            to_node: Target node ID
            to_input: Input field on target

        Raises:
            WorkflowValidationError: If nodes don't exist
            ConnectionError: If connection is invalid
        """
        if from_node not in self.nodes:
            raise WorkflowValidationError(
                f"Source node '{from_node}' not found in workflow"
            )
        if to_node not in self.nodes:
            raise WorkflowValidationError(
                f"Target node '{to_node}' not found in workflow"
            )

        # Self-connection check
        if from_node == to_node:
            raise ConnectionError(f"Cannot connect node '{from_node}' to itself")

        # Add connection to list
        connection = {
            "from_node": from_node,
            "from_output": from_output,
            "to_node": to_node,
            "to_input": to_input,
        }
        self.connections.append(connection)

        logger.info(f"Connected '{from_node}.{from_output}' to '{to_node}.{to_input}'")
        return self

    def connect(
        self,
        from_node: str,
        to_node: str,
        mapping: dict = None,
        from_output: str = None,
        to_input: str = None,
    ) -> None:
        """
        Connect two nodes in the workflow with flexible parameter formats.

        This method provides a more intuitive API for connecting nodes and supports
        both simple connections and complex mapping-based connections.

        Args:
            from_node: Source node ID
            to_node: Target node ID
            mapping: Dict mapping from_output to to_input (e.g., {"data": "input"})
            from_output: Single output field (alternative to mapping)
            to_input: Single input field (alternative to mapping)

        Examples:
            # Simple connection
            workflow.connect("node1", "node2", from_output="data", to_input="input")

            # Mapping-based connection
            workflow.connect("node1", "node2", mapping={"data": "input"})

            # Default data flow
            workflow.connect("node1", "node2")  # Uses "data" -> "data"
        """
        if mapping:
            # Handle mapping-based connections
            for from_out, to_in in mapping.items():
                self.add_connection(from_node, from_out, to_node, to_in)
        elif from_output and to_input:
            # Handle explicit parameter connections
            self.add_connection(from_node, from_output, to_node, to_input)
        else:
            # Default data flow
            self.add_connection(from_node, "data", to_node, "data")

    def set_metadata(self, **kwargs) -> "WorkflowBuilder":
        """
        Set workflow metadata.

        Args:
            **kwargs: Metadata key-value pairs

        Returns:
            Self for chaining
        """
        self._metadata.update(kwargs)
        return self

    def add_workflow_inputs(
        self, input_node_id: str, input_mappings: dict
    ) -> "WorkflowBuilder":
        """
        Map workflow-level inputs to a specific node's parameters.

        Args:
            input_node_id: The node that should receive workflow inputs
            input_mappings: Dict mapping workflow input names to node parameter names

        Returns:
            Self for chaining
        """
        if input_node_id not in self.nodes:
            raise WorkflowValidationError(f"Node '{input_node_id}' not found")

        # Store input mappings in metadata
        if "_workflow_inputs" not in self._metadata:
            self._metadata["_workflow_inputs"] = {}
        self._metadata["_workflow_inputs"][input_node_id] = input_mappings
        return self

    def update_node(
        self, node_id: str, config_updates: dict[str, Any]
    ) -> "WorkflowBuilder":
        """
        Update the configuration of an existing node.

        This is essential for enterprise scenarios like:
        - Dynamic environment-specific configuration
        - Runtime parameter injection
        - Security context updates
        - A/B testing and feature flags

        Args:
            node_id: ID of the node to update
            config_updates: Dictionary of configuration updates to apply

        Returns:
            Self for chaining

        Raises:
            WorkflowValidationError: If node doesn't exist
        """
        if node_id not in self.nodes:
            raise WorkflowValidationError(f"Node '{node_id}' not found in workflow")

        # Deep merge the configuration updates
        if "config" not in self.nodes[node_id]:
            self.nodes[node_id]["config"] = {}

        self.nodes[node_id]["config"].update(config_updates)
        return self

    def build(self, workflow_id: str | None = None, **kwargs) -> Workflow:
        """
        Build and return a Workflow instance.

        Args:
            workflow_id: Workflow identifier (auto-generated if not provided)
            **kwargs: Additional metadata (name, description, version, etc.)

        Returns:
            Configured Workflow instance

        Raises:
            WorkflowValidationError: If workflow building fails
        """
        # Generate ID if not provided
        if workflow_id is None:
            workflow_id = str(uuid.uuid4())

        # Prepare metadata
        metadata = self._metadata.copy()
        metadata.update(kwargs)
        if "name" not in metadata:
            metadata["name"] = f"Workflow-{workflow_id[:8]}"

        # Get basic workflow properties
        name = metadata.pop("name")
        description = metadata.pop("description", "")
        version = metadata.pop("version", "1.0.0")
        author = metadata.pop("author", "")

        # Create workflow
        workflow = Workflow(
            workflow_id=workflow_id,
            name=name,
            description=description,
            version=version,
            author=author,
            metadata=metadata,
        )

        # Add nodes to workflow
        for node_id, node_info in self.nodes.items():
            try:
                if "instance" in node_info:
                    # Node instance was provided
                    workflow.add_node(
                        node_id=node_id, node_or_type=node_info["instance"]
                    )
                elif "class" in node_info:
                    # Node class was provided
                    node_class = node_info["class"]
                    node_config = node_info.get("config", {})
                    workflow.add_node(
                        node_id=node_id, node_or_type=node_class, **node_config
                    )
                else:
                    # String node type
                    node_type = node_info["type"]
                    node_config = node_info.get("config", {})
                    workflow.add_node(
                        node_id=node_id, node_or_type=node_type, **node_config
                    )
            except Exception as e:
                raise WorkflowValidationError(
                    f"Failed to add node '{node_id}' to workflow: {e}"
                ) from e

        # Add connections to workflow
        for conn in self.connections:
            try:
                from_node = conn["from_node"]
                from_output = conn["from_output"]
                to_node = conn["to_node"]
                to_input = conn["to_input"]

                # Add the connection to workflow
                workflow._add_edge_internal(from_node, from_output, to_node, to_input)
            except Exception as e:
                raise WorkflowValidationError(
                    f"Failed to connect '{from_node}' to '{to_node}': {e}"
                ) from e

        # Parameter injection: Find nodes without incoming connections and inject parameters
        if self.workflow_parameters:
            nodes_with_inputs = set()
            for conn in self.connections:
                if not conn.get("is_workflow_input"):
                    nodes_with_inputs.add(conn["to_node"])

            nodes_without_inputs = set(self.nodes.keys()) - nodes_with_inputs

            # For each node without inputs, check if it needs workflow parameters
            for node_id in nodes_without_inputs:
                node_info = self.nodes[node_id]
                node_instance = workflow.get_node(node_id)

                if hasattr(node_instance, "get_parameters"):
                    params = node_instance.get_parameters()

                    # Check which required parameters are missing from config
                    for param_name, param_def in params.items():
                        if param_def.required and param_name not in node_info["config"]:
                            # Check if this parameter should come from workflow parameters
                            if param_name in self.workflow_parameters:
                                # Add to node config
                                node_info["config"][param_name] = (
                                    self.workflow_parameters[param_name]
                                )
                            elif node_id in self.parameter_mappings:
                                # Check parameter mappings
                                mapping = self.parameter_mappings[node_id]
                                if param_name in mapping:
                                    workflow_param = mapping[param_name]
                                    if workflow_param in self.workflow_parameters:
                                        node_info["config"][param_name] = (
                                            self.workflow_parameters[workflow_param]
                                        )

        # Store workflow parameters in metadata for runtime reference
        workflow.metadata["workflow_parameters"] = self.workflow_parameters
        workflow.metadata["parameter_mappings"] = self.parameter_mappings

        logger.info(
            f"Built workflow '{workflow_id}' with "
            f"{len(self.nodes)} nodes and {len(self.connections)} connections"
        )
        return workflow

    def set_workflow_parameters(self, **parameters) -> "WorkflowBuilder":
        """
        Set default parameters that will be passed to all nodes.

        Args:
            **parameters: Key-value pairs of workflow-level parameters

        Returns:
            Self for chaining
        """
        self.workflow_parameters.update(parameters)
        return self

    def add_parameter_mapping(
        self, node_id: str, mappings: dict[str, str]
    ) -> "WorkflowBuilder":
        """
        Add parameter mappings for a specific node.

        Args:
            node_id: Node to configure
            mappings: Dict mapping workflow param names to node param names

        Returns:
            Self for chaining
        """
        if node_id not in self.parameter_mappings:
            self.parameter_mappings[node_id] = {}
        self.parameter_mappings[node_id].update(mappings)
        return self

    def add_input_connection(
        self, to_node: str, to_input: str, from_workflow_param: str
    ) -> "WorkflowBuilder":
        """
        Connect a workflow parameter directly to a node input.

        Args:
            to_node: Target node ID
            to_input: Input parameter name on the node
            from_workflow_param: Workflow parameter name

        Returns:
            Self for chaining
        """
        # Add a special connection type for workflow inputs
        connection = {
            "from_node": "__workflow_input__",
            "from_output": from_workflow_param,
            "to_node": to_node,
            "to_input": to_input,
            "is_workflow_input": True,
        }
        self.connections.append(connection)
        return self

    def clear(self) -> "WorkflowBuilder":
        """
        Clear builder state.

        Returns:
            Self for chaining
        """
        self.nodes = {}
        self.connections = []
        self._metadata = {}
        self.workflow_parameters = {}
        self.parameter_mappings = {}
        return self

    @classmethod
    def from_dict(cls, config: dict[str, Any]) -> "WorkflowBuilder":
        """
        Create builder from dictionary configuration.

        Args:
            config: Dictionary with workflow configuration

        Returns:
            Configured WorkflowBuilder instance

        Raises:
            WorkflowValidationError: If configuration is invalid
        """
        builder = cls()

        # Extract metadata
        for key, value in config.items():
            if key not in ["nodes", "connections"]:
                builder._metadata[key] = value

        # Add nodes - handle both dict and list formats
        nodes_config = config.get("nodes", [])

        if isinstance(nodes_config, dict):
            # Dict format: {node_id: {type: "...", parameters: {...}}}
            for node_id, node_config in nodes_config.items():
                node_type = node_config.get("type")

                # Handle parameter naming inconsistencies - prefer 'parameters' over 'config'
                if "parameters" in node_config:
                    node_params = node_config["parameters"]
                elif "config" in node_config:
                    node_params = node_config["config"]
                else:
                    node_params = {}

                # Ensure node_params is a dictionary
                if not isinstance(node_params, dict):
                    logger.warning(
                        f"Node '{node_id}' parameters must be a dict, got {type(node_params)}. Using empty dict."
                    )
                    node_params = {}

                if not node_type:
                    raise WorkflowValidationError(
                        f"Node type is required for node '{node_id}'"
                    )

                builder.add_node(node_type, node_id, node_params)
        else:
            # List format: [{id: "...", type: "...", config: {...}}]
            for node_config in nodes_config:
                node_id = node_config.get("id")
                node_type = node_config.get("type")

                # Handle parameter naming inconsistencies - prefer 'parameters' over 'config'
                if "parameters" in node_config:
                    node_params = node_config["parameters"]
                elif "config" in node_config:
                    node_params = node_config["config"]
                else:
                    node_params = {}

                # Ensure node_params is a dictionary
                if not isinstance(node_params, dict):
                    logger.warning(
                        f"Node '{node_id}' parameters must be a dict, got {type(node_params)}. Using empty dict."
                    )
                    node_params = {}

                if not node_id:
                    raise WorkflowValidationError("Node ID is required")
                if not node_type:
                    raise WorkflowValidationError(
                        f"Node type is required for node '{node_id}'"
                    )

                builder.add_node(node_type, node_id, node_params)

        # Add connections - handle both full and simple formats
        for conn in config.get("connections", []):
            # Try full format first: {from_node, from_output, to_node, to_input}
            from_node = conn.get("from_node")
            from_output = conn.get("from_output")
            to_node = conn.get("to_node")
            to_input = conn.get("to_input")

            # Handle simple format: {from, to} with default outputs/inputs
            if not from_node:
                from_node = conn.get("from")
                from_output = conn.get("from_output", "result")  # Default output
            if not to_node:
                to_node = conn.get("to")
                to_input = conn.get("to_input", "input")  # Default input

            if not all([from_node, to_node]):
                raise WorkflowValidationError(
                    f"Invalid connection: missing from_node and to_node. Connection data: {conn}"
                )

            # Use defaults if not specified
            from_output = from_output or "result"
            to_input = to_input or "input"

            builder.add_connection(from_node, from_output, to_node, to_input)

        return builder
