import itertools
from collections import defaultdict
from copy import copy, deepcopy
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from uuid import uuid4

import networkx as nx
from networkx import DiGraph

from inference.core import logger
from inference.core.workflows.errors import (
    AssumptionError,
    BlockInterfaceError,
    ControlFlowDefinitionError,
    ExecutionGraphStructureError,
    InvalidReferenceTargetError,
    StepInputDimensionalityError,
    StepInputLineageError,
    WorkflowBlockError,
)
from inference.core.workflows.execution_engine.constants import (
    NODE_COMPILATION_OUTPUT_PROPERTY,
    PARSED_NODE_INPUT_SELECTORS_PROPERTY,
    TOP_LEVEL_LINEAGES_KEY,
    WORKFLOW_INPUT_BATCH_LINEAGE_ID,
)
from inference.core.workflows.execution_engine.entities.base import (
    InputType,
    JsonField,
    OutputDefinition,
)
from inference.core.workflows.execution_engine.entities.types import (
    STEP_AS_SELECTED_ELEMENT,
    Kind,
)
from inference.core.workflows.execution_engine.introspection.entities import (
    ParsedSelector,
)
from inference.core.workflows.execution_engine.introspection.selectors_parser import (
    get_step_selectors,
)
from inference.core.workflows.execution_engine.profiling.core import (
    WorkflowsProfiler,
    execution_phase,
)
from inference.core.workflows.execution_engine.v1.compiler.entities import (
    AutoBatchCastingConfig,
    CompoundStepInputDefinition,
    DictOfStepInputDefinitions,
    DynamicStepInputDefinition,
    ExecutionGraphNode,
    InputDimensionalitySpecification,
    InputNode,
    ListOfStepInputDefinitions,
    NodeCategory,
    NodeInputCategory,
    OutputNode,
    ParameterSpecification,
    ParsedWorkflowDefinition,
    PropertyPredecessorDefinition,
    StaticStepInputDefinition,
    StepInputData,
    StepInputDefinition,
    StepNode,
)
from inference.core.workflows.execution_engine.v1.compiler.graph_traversal import (
    traverse_graph_ensuring_parents_are_reached_first,
)
from inference.core.workflows.execution_engine.v1.compiler.reference_type_checker import (
    validate_reference_kinds,
)
from inference.core.workflows.execution_engine.v1.compiler.utils import (
    construct_input_selector,
    construct_output_selector,
    construct_step_selector,
    get_last_chunk_of_selector,
    get_nodes_of_specific_category,
    get_step_selector_from_its_output,
    identify_lineage,
    is_control_flow_step,
    is_input_node,
    is_input_selector,
    is_output_node,
    is_step_node,
    is_step_output_selector,
    is_step_selector,
    node_as,
)
from inference.core.workflows.prototypes.block import WorkflowBlockManifest

NODE_DEFINITION_KEY = "definition"
STEP_INPUT_SELECTORS_PROPERTY = "step_input_selectors"
EXCLUDED_FIELDS = {"type", "name"}


@execution_phase(
    name="execution_graph_creation",
    categories=["execution_engine_operation"],
)
def prepare_execution_graph(
    workflow_definition: ParsedWorkflowDefinition,
    profiler: Optional[WorkflowsProfiler] = None,
) -> DiGraph:
    execution_graph = construct_graph(
        workflow_definition=workflow_definition,
    )
    if not nx.is_directed_acyclic_graph(execution_graph):
        raise ExecutionGraphStructureError(
            public_message=f"Detected cycle in execution graph. This means that there is output from one "
            f"step that is connected to input of other step creating loop in the graph that would "
            f"never end if executed.",
            context="workflow_compilation | execution_graph_construction",
        )
    return denote_data_flow_in_workflow(
        execution_graph=execution_graph,
        parsed_workflow_definition=workflow_definition,
    )


def construct_graph(
    workflow_definition: ParsedWorkflowDefinition,
) -> DiGraph:
    execution_graph = nx.DiGraph()
    execution_graph = add_input_nodes_for_graph(
        inputs=workflow_definition.inputs,
        execution_graph=execution_graph,
    )
    execution_graph = add_steps_nodes_for_graph(
        steps=workflow_definition.steps,
        execution_graph=execution_graph,
    )
    execution_graph = add_output_nodes_for_graph(
        outputs=workflow_definition.outputs,
        execution_graph=execution_graph,
    )
    execution_graph = add_steps_edges(
        workflow_definition=workflow_definition,
        execution_graph=execution_graph,
    )
    return add_edges_for_outputs(
        workflow_definition=workflow_definition,
        execution_graph=execution_graph,
    )


def add_input_nodes_for_graph(
    inputs: List[InputType],
    execution_graph: DiGraph,
) -> DiGraph:
    for input_spec in inputs:
        input_selector = construct_input_selector(input_name=input_spec.name)
        if input_spec.is_batch_oriented():
            if input_spec.dimensionality < 1:
                raise ExecutionGraphStructureError(
                    public_message=f"Detected batch oriented input `{input_spec.name}` with "
                    f"declared dimensionality `{input_spec.dimensionality}` which is below "
                    f"one (one is minimum dimensionality of the batch). Fix input definition in"
                    f"your Workflow.",
                    context="workflow_compilation | execution_graph_construction",
                )
            data_lineage = [WORKFLOW_INPUT_BATCH_LINEAGE_ID]
            for _ in range(input_spec.dimensionality - 1):
                # TODO: this may end up being a bug - with ability for multi-step debugging, if we will
                #  ever have a situation that there will be multiple step outputs with nested
                #  dimensionality with the same lineage, this re-construction method will
                #  assign a different lineage identifier, causing the inputs being non-composable in
                #  a single execution branch
                data_lineage.append(f"{uuid4()}")
        else:
            data_lineage = []
        compilation_output = InputNode(
            node_category=NodeCategory.INPUT_NODE,
            name=input_spec.name,
            selector=input_selector,
            data_lineage=data_lineage,
            input_manifest=input_spec,
        )
        execution_graph.add_node(
            input_selector,
            **{
                NODE_COMPILATION_OUTPUT_PROPERTY: compilation_output,
            },
        )
    return execution_graph


def add_steps_nodes_for_graph(
    steps: List[WorkflowBlockManifest],
    execution_graph: DiGraph,
) -> DiGraph:
    for step in steps:
        step_selector = construct_step_selector(step_name=step.name)
        compilation_output = StepNode(
            node_category=NodeCategory.STEP_NODE,
            name=step.name,
            selector=step_selector,
            data_lineage=[],
            step_manifest=step,
        )
        execution_graph.add_node(
            step_selector,
            **{
                NODE_COMPILATION_OUTPUT_PROPERTY: compilation_output,
            },
        )
    return execution_graph


def add_output_nodes_for_graph(
    outputs: List[JsonField],
    execution_graph: DiGraph,
) -> DiGraph:
    for output_spec in outputs:
        output_selector = construct_output_selector(name=output_spec.name)
        compilation_output = OutputNode(
            node_category=NodeCategory.OUTPUT_NODE,
            name=output_spec.name,
            selector=output_selector,
            data_lineage=[],
            output_manifest=output_spec,
        )
        execution_graph.add_node(
            output_selector,
            **{
                NODE_COMPILATION_OUTPUT_PROPERTY: compilation_output,
            },
        )
    return execution_graph


def add_steps_edges(
    workflow_definition: ParsedWorkflowDefinition,
    execution_graph: DiGraph,
) -> DiGraph:
    for step in workflow_definition.steps:
        source_step_selector = construct_step_selector(step_name=step.name)
        step_selectors = get_step_selectors(step_manifest=step)
        execution_graph.nodes[source_step_selector][
            PARSED_NODE_INPUT_SELECTORS_PROPERTY
        ] = step_selectors
        execution_graph = add_edges_for_step(
            execution_graph=execution_graph,
            source_step_selector=source_step_selector,
            target_step_parsed_selectors=step_selectors,
        )
    return execution_graph


def add_edges_for_step(
    execution_graph: DiGraph,
    source_step_selector: str,
    target_step_parsed_selectors: List[ParsedSelector],
) -> DiGraph:
    for target_step_parsed_selector in target_step_parsed_selectors:
        execution_graph = add_edge_for_step(
            execution_graph=execution_graph,
            source_step_selector=source_step_selector,
            target_step_parsed_selector=target_step_parsed_selector,
        )
    return execution_graph


def add_edge_for_step(
    execution_graph: DiGraph,
    source_step_selector: str,
    target_step_parsed_selector: ParsedSelector,
) -> DiGraph:
    target_step_selector = get_step_selector_from_its_output(
        step_output_selector=target_step_parsed_selector.value
    )
    verify_edge_is_created_between_existing_nodes(
        execution_graph=execution_graph,
        start=source_step_selector,
        end=target_step_selector,
    )
    if is_step_selector(target_step_parsed_selector.value):
        return establish_control_flow_edge(
            source_step_selector=source_step_selector,
            target_step_parsed_selector=target_step_parsed_selector,
            execution_graph=execution_graph,
        )
    if is_input_selector(target_step_parsed_selector.value):
        input_node_compilation_data = node_as(
            execution_graph=execution_graph,
            node=target_step_parsed_selector.value,
            expected_type=InputNode,
        )
        actual_input_kind = input_node_compilation_data.input_manifest.kind
    else:
        other_step_compilation_data = node_as(
            execution_graph=execution_graph,
            node=target_step_selector,
            expected_type=StepNode,
        )
        source_step_compilation_data = node_as(
            execution_graph=execution_graph,
            node=source_step_selector,
            expected_type=StepNode,
        )
        actual_input_kind = get_kind_of_value_provided_in_step_output(
            step_manifest=other_step_compilation_data.step_manifest,
            step_property=get_last_chunk_of_selector(
                selector=target_step_parsed_selector.value
            ),
            source_step_manifest=source_step_compilation_data.step_manifest,
        )
    expected_input_kind = list(
        itertools.chain.from_iterable(
            ref.kind
            for ref in target_step_parsed_selector.definition.allowed_references
        )
    )
    error_message = (
        f"Failed to validate reference provided for step: {source_step_selector} regarding property: "
        f"{target_step_parsed_selector.definition.property_name} with value: {target_step_parsed_selector.value}. "
        f"Allowed kinds of references for this property: {list(set(e.name for e in expected_input_kind))}. "
        f"Types of output for referred property: "
        f"{list(set(a.name if isinstance(a, Kind) else a for a in actual_input_kind))}"
    )
    validate_reference_kinds(
        expected=expected_input_kind,
        actual=actual_input_kind,
        error_message=error_message,
    )
    if execution_graph.has_edge(target_step_selector, source_step_selector):
        edge_data = execution_graph.edges[(target_step_selector, source_step_selector)]
        if STEP_INPUT_SELECTORS_PROPERTY not in edge_data:
            edge_data[STEP_INPUT_SELECTORS_PROPERTY] = []
        edge_data[STEP_INPUT_SELECTORS_PROPERTY].append(
            target_step_parsed_selector,
        )
    else:
        execution_graph.add_edge(
            target_step_selector,
            source_step_selector,
            **{STEP_INPUT_SELECTORS_PROPERTY: [target_step_parsed_selector]},
        )
    return execution_graph


def establish_control_flow_edge(
    source_step_selector: str,
    target_step_parsed_selector: ParsedSelector,
    execution_graph: DiGraph,
) -> DiGraph:
    if not step_definition_allows_control_flow_references(
        parsed_selector=target_step_parsed_selector
    ):
        raise ExecutionGraphStructureError(
            public_message=f"Detected reference to other step in manifest of step {source_step_selector}. "
            f"This is not allowed, as step manifest does not define any properties of type `StepSelector` "
            f"allowing for defining connections between steps that represent flow control in `workflows`.",
            context="workflow_compilation | execution_graph_construction",
        )
    target_step_selector = get_step_selector_from_its_output(
        step_output_selector=target_step_parsed_selector.value
    )
    source_compilation_data = node_as(
        execution_graph=execution_graph,
        node=source_step_selector,
        expected_type=StepNode,
    )
    target_compilation_data = node_as(
        execution_graph=execution_graph,
        node=target_step_selector,
        expected_type=StepNode,
    )
    nodes_categories = {
        source_compilation_data.node_category,
        target_compilation_data.node_category,
    }
    if nodes_categories != {NodeCategory.STEP_NODE}:
        raise ExecutionGraphStructureError(
            public_message=f"Attempted to create flow-control connection between workflow nodes: "
            f"{source_step_selector} and {target_step_selector}, one of which is not denoted"
            f"as step node.",
            context="workflow_compilation | execution_graph_construction",
        )
    execution_graph.add_edge(
        source_step_selector,
        target_step_selector,
    )
    if target_step_selector in source_compilation_data.child_execution_branches:
        raise ExecutionGraphStructureError(
            public_message=f"While establishing flow-control connection between source `{source_step_selector}` "
            f"and target `{target_step_selector}` it was discovered that source step defines"
            f"control flow transition into target one more than once, which is not allowed as "
            f"that situation creates ambiguity in selecting execution branch.",
            context="workflow_compilation | execution_graph_construction",
        )
    execution_branch_name = f"Branch[{source_step_selector} -> {target_step_parsed_selector.definition.property_name}]"
    source_compilation_data.child_execution_branches[target_step_selector] = (
        execution_branch_name
    )
    target_compilation_data.execution_branches_impacting_inputs.add(
        execution_branch_name
    )
    return execution_graph


def step_definition_allows_control_flow_references(
    parsed_selector: ParsedSelector,
) -> bool:
    return any(
        definition.selected_element == STEP_AS_SELECTED_ELEMENT
        for definition in parsed_selector.definition.allowed_references
    )


def get_kind_of_value_provided_in_step_output(
    step_manifest: WorkflowBlockManifest,
    step_property: str,
    source_step_manifest: WorkflowBlockManifest,
) -> List[Kind]:
    referred_node_outputs = step_manifest.get_actual_outputs()
    actual_kind = []
    matched_property = False
    for output in referred_node_outputs:
        if output.name != step_property:
            continue
        matched_property = True
        actual_kind.extend(output.kind)
    if not matched_property:
        property_name = get_property_with_invalid_selector(
            manifest=source_step_manifest,
            step_property=step_property,
        )
        raise ExecutionGraphStructureError(
            public_message=f"Found reference to non-existing property `{step_property}` of step `{step_manifest.name}`.",
            context="workflow_compilation | execution_graph_construction",
            blocks_errors=[
                WorkflowBlockError(
                    block_id=source_step_manifest.name,
                    block_type=source_step_manifest.type,
                    property_name=property_name,
                    property_details=f"'{step_manifest.name}.{step_property}' is an invalid reference",
                )
            ],
        )
    return actual_kind


def add_edges_for_step_inputs(
    execution_graph: DiGraph,
    input_selectors: Set[str],
    step_selector: str,
) -> DiGraph:
    for input_selector in input_selectors:
        if is_step_output_selector(selector_or_value=input_selector):
            input_selector = get_step_selector_from_its_output(
                step_output_selector=input_selector
            )
        verify_edge_is_created_between_existing_nodes(
            execution_graph=execution_graph,
            start=input_selector,
            end=step_selector,
        )
        execution_graph.add_edge(input_selector, step_selector)
    return execution_graph


def add_edges_for_outputs(
    workflow_definition: ParsedWorkflowDefinition,
    execution_graph: DiGraph,
) -> DiGraph:
    for output in workflow_definition.outputs:
        node_selector = output.selector
        if is_step_output_selector(selector_or_value=node_selector):
            node_selector = get_step_selector_from_its_output(
                step_output_selector=node_selector
            )
        output_selector = construct_output_selector(name=output.name)
        verify_edge_is_created_between_existing_nodes(
            execution_graph=execution_graph,
            start=node_selector,
            end=output_selector,
        )
        output_node_manifest = node_as(
            execution_graph=execution_graph,
            node=output_selector,
            expected_type=OutputNode,
        )
        if is_step_output_selector(selector_or_value=output.selector):
            step_manifest = execution_graph.nodes[node_selector][
                NODE_COMPILATION_OUTPUT_PROPERTY
            ].step_manifest
            step_outputs = step_manifest.get_actual_outputs()
            denote_output_node_kind_based_on_step_outputs(
                output_selector=output.selector,
                step_outputs=step_outputs,
                output_node_manifest=output_node_manifest,
            )
        else:
            input_manifest = node_as(
                execution_graph=execution_graph,
                node=node_selector,
                expected_type=InputNode,
            ).input_manifest
            output_node_manifest.kind = copy(input_manifest.kind)
        execution_graph.add_edge(node_selector, output_selector)
    return execution_graph


def verify_edge_is_created_between_existing_nodes(
    execution_graph: DiGraph,
    start: str,
    end: str,
) -> None:
    if not execution_graph.has_node(start) or not execution_graph.has_node(end):
        invalid_block = start if not execution_graph.has_node(start) else end
        referencing_block = end if invalid_block == start else start

        step_type = None
        property_name = None
        if execution_graph.has_node(referencing_block):
            if is_step_node(execution_graph, referencing_block):
                step_manifest = node_as(
                    execution_graph=execution_graph,
                    node=referencing_block,
                    expected_type=StepNode,
                ).step_manifest
                step_type = step_manifest.type
                property_name = get_property_with_invalid_selector(
                    manifest=step_manifest,
                    step_property=invalid_block,
                )

        block_id = (
            "$outputs"
            if referencing_block.startswith("$outputs.")
            else get_last_chunk_of_selector(selector=referencing_block)
        )
        property_name = (
            get_last_chunk_of_selector(selector=referencing_block)
            if referencing_block.startswith("$outputs.")
            else property_name
        )

        is_input = is_input_selector(selector_or_value=invalid_block)
        raise InvalidReferenceTargetError(
            public_message=f"Workflow references '{invalid_block}' which points to a non-existent {is_input and 'input parameter' or 'block'}.",
            context="workflow_compilation | execution_graph_construction",
            blocks_errors=[
                WorkflowBlockError(
                    block_id=block_id,
                    block_type=step_type,
                    property_name=property_name,
                    property_details=f"Invalid reference to {invalid_block}",
                ),
            ],
        )


def denote_output_node_kind_based_on_step_outputs(
    output_selector: str,
    step_outputs: List[OutputDefinition],
    output_node_manifest: OutputNode,
) -> None:
    selected_output_name = get_last_chunk_of_selector(selector=output_selector)
    kinds_for_outputs = {output.name: output.kind for output in step_outputs}

    if selected_output_name == "*":
        output_node_manifest.kind = deepcopy(kinds_for_outputs)
        return None
    if selected_output_name not in kinds_for_outputs:
        output_name = output_node_manifest.output_manifest.name

        raise InvalidReferenceTargetError(
            public_message=f"Workflow output references '{output_selector}' which points to a non-existent block property.",
            context="workflow_compilation | execution_graph_construction",
            blocks_errors=[
                WorkflowBlockError(
                    block_id="$outputs",
                    property_name=output_name,
                    property_details=f"Invalid reference to {output_selector}",
                ),
            ],
        )
    output_node_manifest.kind = copy(kinds_for_outputs[selected_output_name])
    return None


def denote_data_flow_in_workflow(
    execution_graph: DiGraph,
    parsed_workflow_definition: ParsedWorkflowDefinition,
) -> nx.DiGraph:
    block_manifest_by_step_name = {
        step.name: step for step in parsed_workflow_definition.steps
    }
    super_input_node = "<super-input>"
    execution_graph = add_super_input_node_in_execution_graph(
        execution_graph=execution_graph,
        super_input_node=super_input_node,
    )
    execution_graph.nodes[super_input_node][NODE_COMPILATION_OUTPUT_PROPERTY] = (
        InputNode(
            node_category=NodeCategory.INPUT_NODE,
            name=super_input_node,
            selector=f"$inputs.{super_input_node}",
            data_lineage=[],
            input_manifest=None,  # this is expected never to be reached
        )
    )
    top_level_data_lineage = set()
    for node in traverse_graph_ensuring_parents_are_reached_first(
        graph=execution_graph,
        start_node=super_input_node,
    ):
        execution_graph = denote_data_flow_for_node(
            execution_graph=execution_graph,
            node=node,
            block_manifest_by_step_name=block_manifest_by_step_name,
            on_top_level_lineage_denoted=lambda element: top_level_data_lineage.add(
                element
            ),
        )
    execution_graph.remove_node(super_input_node)
    execution_graph.graph[TOP_LEVEL_LINEAGES_KEY] = top_level_data_lineage
    return execution_graph


def denote_data_flow_for_node(
    execution_graph: DiGraph,
    node: str,
    block_manifest_by_step_name: Dict[str, WorkflowBlockManifest],
    on_top_level_lineage_denoted: Callable[[str], None],
) -> DiGraph:
    if is_input_node(execution_graph=execution_graph, node=node):
        # everything already set there, in the previous stage of compilation
        return execution_graph
    if is_step_node(execution_graph=execution_graph, node=node):
        step_name = get_last_chunk_of_selector(selector=node)
        if step_name not in block_manifest_by_step_name:
            raise AssumptionError(
                public_message=f"Workflow Compiler expected manifest for the step: {step_name} to be registered "
                f"but this condition is not met. This is most likely the bug. "
                f"Contact Roboflow team through github issues "
                f"(https://github.com/roboflow/inference/issues) providing full "
                f"context of the problem - including workflow definition you use.",
                context="workflow_compilation | execution_graph_construction | retrieving_predecessors_for_output",
            )
        manifest = block_manifest_by_step_name[step_name]
        return denote_data_flow_for_step(
            execution_graph=execution_graph,
            node=node,
            manifest=manifest,
            on_top_level_lineage_denoted=on_top_level_lineage_denoted,
        )
    if is_output_node(execution_graph=execution_graph, node=node):
        # output is allowed to have exactly one predecessor
        output_predecessors = list(execution_graph.predecessors(node))
        if len(output_predecessors) != 1:
            raise AssumptionError(
                public_message=f"Workflow Compiler expected each output in compiled graph to have one predecessor, "
                f"but this condition is not met for node {node}. This is most likely the bug. "
                f"Contact Roboflow team through github issues "
                f"(https://github.com/roboflow/inference/issues) providing full "
                f"context of the problem - including workflow definition you use.",
                context="workflow_compilation | execution_graph_construction | retrieving_predecessors_for_output",
            )
        predecessor_node = output_predecessors[0]
        predecessor_node_data = node_as(
            execution_graph=execution_graph,
            node=predecessor_node,
            expected_type=ExecutionGraphNode,
        )
        predecessor_node_lineage = predecessor_node_data.data_lineage
        output_node_data = node_as(
            execution_graph=execution_graph,
            node=node,
            expected_type=OutputNode,
        )
        output_node_data.data_lineage = predecessor_node_lineage
        return execution_graph
    raise AssumptionError(
        f"Workflow Compiler encountered node: {node} which cannot be classified as known node type. "
        f"This is most likely the bug. Contact Roboflow team through github issues "
        f"(https://github.com/roboflow/inference/issues) providing full "
        f"context of the problem - including workflow definition you use.",
        context="workflow_compilation | execution_graph_construction | denoting_data_flow_for_step",
    )


def denote_data_flow_for_step(
    execution_graph: DiGraph,
    node: str,
    manifest: WorkflowBlockManifest,
    on_top_level_lineage_denoted: Callable[[str], None],
) -> DiGraph:
    all_control_flow_predecessors, all_non_control_flow_predecessors = (
        separate_control_flow_predecessors_from_data_providers(
            execution_graph=execution_graph, node=node
        )
    )
    input_data = build_input_data_for_step(
        manifest=manifest,
        step_node=node,
        data_providing_predecessors=all_non_control_flow_predecessors,
        execution_graph=execution_graph,
    )
    step_name = get_last_chunk_of_selector(node)
    step_node_data = node_as(
        execution_graph=execution_graph,
        node=node,
        expected_type=StepNode,
    )
    parsed_step_input_selectors: List[ParsedSelector] = execution_graph.nodes[node][
        PARSED_NODE_INPUT_SELECTORS_PROPERTY
    ]
    batch_compatibility_of_properties = retrieve_batch_compatibility_of_input_selectors(
        input_selectors=parsed_step_input_selectors
    )
    scalar_parameters_to_be_batched = (
        verify_declared_batch_compatibility_against_actual_inputs(
            node=node,
            step_node_data=step_node_data,
            input_data=input_data,
            batch_compatibility_of_properties=batch_compatibility_of_properties,
        )
    )
    input_dimensionality_offsets = manifest.get_input_dimensionality_offsets()
    verify_step_input_dimensionality_offsets(
        step_name=step_name,
        input_dimensionality_offsets=input_dimensionality_offsets,
    )
    inputs_dimensionalities = get_inputs_dimensionalities(
        step_name=step_name,
        step_type=manifest.type,
        input_data=input_data,
        scalar_parameters_to_be_batched=scalar_parameters_to_be_batched,
        input_dimensionality_offsets=input_dimensionality_offsets,
    )
    logger.debug(
        f"For step: {node}, detected the following input dimensionalities: {inputs_dimensionalities}"
    )
    parameters_with_batch_inputs = grab_parameters_defining_batch_inputs(
        inputs_dimensionalities=inputs_dimensionalities,
    )
    dimensionality_reference_property = manifest.get_dimensionality_reference_property()
    output_dimensionality_offset = manifest.get_output_dimensionality_offset()
    verify_step_input_dimensionality_offsets(
        step_name=step_name,
        input_dimensionality_offsets=input_dimensionality_offsets,
    )
    verify_output_offset(
        step_name=step_name,
        parameters_with_batch_inputs=parameters_with_batch_inputs,
        dimensionality_reference_property=dimensionality_reference_property,
        input_dimensionality_offsets=input_dimensionality_offsets,
        output_dimensionality_offset=output_dimensionality_offset,
    )
    verify_input_data_dimensionality(
        step_name=step_name,
        step_type=manifest.type,
        dimensionality_reference_property=dimensionality_reference_property,
        inputs_dimensionalities=inputs_dimensionalities,
        dimensionality_offstes=input_dimensionality_offsets,
    )
    all_data_derived_lineages = get_input_data_lineage_excluding_auto_batch_casting(
        step_name=step_name,
        input_data=input_data,
        scalar_parameters_to_be_batched=scalar_parameters_to_be_batched,
    )
    all_control_flow_lineages = get_lineage_derived_from_control_flow(
        control_flow_steps_selectors=all_control_flow_predecessors,
        execution_graph=execution_graph,
    )
    verify_compatibility_of_input_data_lineage_with_control_flow_lineage(
        step_name=step_name,
        inputs_lineage=all_data_derived_lineages,
        control_flow_steps_selectors=all_control_flow_predecessors,
        execution_graph=execution_graph,
    )
    step_node_data.input_data = input_data
    step_node_data.dimensionality_reference_property = dimensionality_reference_property
    step_node_data.control_flow_lineage_dims = [
        len(_lineage) for _lineage in all_control_flow_lineages if _lineage
    ]
    step_node_data.batch_oriented_parameters = parameters_with_batch_inputs
    if not all_data_derived_lineages and not all_control_flow_lineages:
        if manifest.get_output_dimensionality_offset() > 0:
            # brave decision to open a Pandora box
            data_lineage = [node]
        else:
            data_lineage = []
        control_flow_lineage_support = []
    else:
        data_lineage, control_flow_lineage_support = (
            establish_batch_oriented_step_lineage(
                step_selector=node,
                all_data_derived_lineages=all_data_derived_lineages,
                all_control_flow_lineages=all_control_flow_lineages,
                input_data=input_data,
                dimensionality_reference_property=dimensionality_reference_property,
                output_dimensionality_offset=output_dimensionality_offset,
            )
        )
    step_node_data.step_execution_dimensionality = (
        establish_step_execution_dimensionality(
            inputs_dimensionalities=inputs_dimensionalities,
            control_flow_lineage_support=control_flow_lineage_support,
            output_dimensionality_offset=output_dimensionality_offset,
        )
    )
    lineage_supports = get_lineage_support_for_auto_batch_casted_parameters(
        input_dimensionalities=inputs_dimensionalities,
        all_lineages_of_batch_parameters=all_data_derived_lineages,
        scalar_parameters_to_be_batched=scalar_parameters_to_be_batched,
    )
    step_node_data.auto_batch_casting_lineage_supports = lineage_supports
    step_node_data.control_flow_lineage_support = control_flow_lineage_support
    if data_lineage:
        on_top_level_lineage_denoted(data_lineage[0])
    step_node_data.data_lineage = data_lineage
    return execution_graph


def _collect_unique_control_flow_lineages_with_step_mapping(
    control_flow_steps_selectors: List[str],
    execution_graph: nx.DiGraph,
) -> Tuple[List[List[str]], Dict[int, List[str]]]:
    """
    Collect data lineages from control flow steps and deduplicate by lineage id.

    For each given control flow step selector, reads its data_lineage from the
    execution graph. Returns unique non-empty lineages (by identify_lineage id)
    and a mapping from lineage id to every step selector that produced that
    lineage (used e.g. for error reporting when lineage is incompatible).

    Args:
        control_flow_steps_selectors: Step selectors (node ids) of control
            flow steps whose data_lineage is to be collected.
        execution_graph: The workflow execution graph containing step nodes
            and their data_lineage.

    Returns:
        A tuple of (unique_lineages, lineage_id_to_step_selectors):
        - unique_lineages: list of distinct non-empty data lineages.
        - lineage_id_to_step_selectors: map from lineage id to list of step
          selectors whose data_lineage has that id (includes empty lineages).
    """
    unique_lineages: List[List[str]] = []
    seen_lineage_ids: Set[int] = set()
    lineage_id_to_step_selectors: Dict[int, List[str]] = defaultdict(list)

    for step_selector in control_flow_steps_selectors:
        step_data = node_as(
            execution_graph=execution_graph,
            node=step_selector,
            expected_type=StepNode,
        )
        lineage = step_data.data_lineage
        lineage_id = identify_lineage(lineage)
        lineage_id_to_step_selectors[lineage_id].append(step_selector)
        if lineage and lineage_id not in seen_lineage_ids:
            seen_lineage_ids.add(lineage_id)
            unique_lineages.append(lineage)

    return unique_lineages, dict(lineage_id_to_step_selectors)


def get_lineage_derived_from_control_flow(
    control_flow_steps_selectors: List[str],
    execution_graph: nx.DiGraph,
) -> List[List[str]]:
    """
    Return unique non-empty data lineages from the given control flow steps.

    Each lineage is taken from the step's data_lineage in the execution graph.
    Lineages are deduplicated by lineage id (see identify_lineage); empty
    lineages are omitted. Used when establishing batch-oriented step lineage.

    Args:
        control_flow_steps_selectors: Step selectors (node ids) of control
            flow steps whose data_lineage is to be collected.
        execution_graph: The workflow execution graph containing step nodes
            and their data_lineage.

    Returns:
        List of distinct non-empty data lineages, one per unique lineage id.
    """
    unique_lineages, _ = _collect_unique_control_flow_lineages_with_step_mapping(
        control_flow_steps_selectors=control_flow_steps_selectors,
        execution_graph=execution_graph,
    )
    return unique_lineages


def separate_control_flow_predecessors_from_data_providers(
    execution_graph: DiGraph,
    node: str,
) -> Tuple[List[str], List[str]]:
    all_predecessors = list(execution_graph.predecessors(node))
    all_control_flow_predecessors = [
        predecessor
        for predecessor in all_predecessors
        if is_control_flow_step(execution_graph=execution_graph, node=predecessor)
    ]
    all_non_control_flow_predecessors = [
        predecessor
        for predecessor in all_predecessors
        if not is_control_flow_step(execution_graph=execution_graph, node=predecessor)
    ]
    return all_control_flow_predecessors, all_non_control_flow_predecessors


def build_input_data_for_step(
    manifest: WorkflowBlockManifest,
    step_node: str,
    data_providing_predecessors: List[str],
    execution_graph: DiGraph,
) -> StepInputData:
    predecessors_by_property_name = get_predecessors_for_step_manifest_properties(
        execution_graph=execution_graph,
        step_node=step_node,
        data_providing_predecessors=data_providing_predecessors,
    )
    manifest_fields_values = get_manifest_fields_values(step_manifest=manifest)
    result = {}
    for name, value in manifest_fields_values.items():
        result[name] = build_input_property(
            step_node=step_node,
            manifest_property_name=name,
            manifest_property_value=value,
            predecessors_by_property_name=predecessors_by_property_name,
            execution_graph=execution_graph,
        )
    return result


def get_predecessors_for_step_manifest_properties(
    execution_graph: DiGraph, step_node: str, data_providing_predecessors: List[str]
) -> Dict[str, List[PropertyPredecessorDefinition]]:
    predecessors_by_property_name = defaultdict(list)
    for predecessor in data_providing_predecessors:
        edge_data = execution_graph.edges[(predecessor, step_node)]
        # STEP_INPUT_SELECTORS_PROPERTY is optional, as special <super-input> node may be directly
        # connected to step
        selectors_associated_to_edge: List[ParsedSelector] = edge_data.get(
            STEP_INPUT_SELECTORS_PROPERTY, []
        )
        for selector_associated_to_edge in selectors_associated_to_edge:
            definition = PropertyPredecessorDefinition(
                predecessor_selector=predecessor,
                parsed_selector=selector_associated_to_edge,
            )
            predecessors_by_property_name[
                selector_associated_to_edge.definition.property_name
            ].append(definition)
    return predecessors_by_property_name


def build_input_property(
    step_node: str,
    manifest_property_name: str,
    manifest_property_value: Any,
    predecessors_by_property_name: Dict[str, List[PropertyPredecessorDefinition]],
    execution_graph: DiGraph,
) -> Union[StepInputDefinition, CompoundStepInputDefinition]:
    if manifest_property_name not in predecessors_by_property_name:
        return StaticStepInputDefinition(
            parameter_specification=ParameterSpecification(
                parameter_name=manifest_property_name,
            ),
            category=NodeInputCategory.STATIC_VALUE,
            value=manifest_property_value,
        )
    if isinstance(manifest_property_value, dict):
        return build_nested_dictionary_for_input_property(
            step_node=step_node,
            manifest_property_name=manifest_property_name,
            manifest_property_value=manifest_property_value,
            predecessors_definitions=predecessors_by_property_name[
                manifest_property_name
            ],
            execution_graph=execution_graph,
        )
    if isinstance(manifest_property_value, list):
        return build_nested_list_for_input_property(
            step_node=step_node,
            manifest_property_name=manifest_property_name,
            manifest_property_value=manifest_property_value,
            predecessors_definitions=predecessors_by_property_name[
                manifest_property_name
            ],
            execution_graph=execution_graph,
        )
    matching_predecessors_data = predecessors_by_property_name[manifest_property_name]
    if len(matching_predecessors_data) != 1:
        raise AssumptionError(
            public_message=f"Workflow Compiler deduced that property `{manifest_property_name}` of step `{step_node}` "
            f"should be fed with data coming from exactly one execution graph node, but found "
            f"{len(matching_predecessors_data)} data sources. This is most likely the bug. "
            f"Contact Roboflow team through github issues "
            f"(https://github.com/roboflow/inference/issues) providing full "
            f"context of the problem - including workflow definition you use.",
            context="workflow_compilation | execution_graph_construction | collecting_step_inputs",
        )
    predecessor_selector, predecessor_parsed_selector = (
        matching_predecessors_data[0].predecessor_selector,
        matching_predecessors_data[0].parsed_selector,
    )
    if is_input_node(execution_graph=execution_graph, node=predecessor_selector):
        predecessor_node_data = node_as(
            execution_graph=execution_graph,
            node=matching_predecessors_data[0].predecessor_selector,
            expected_type=InputNode,
        )
        category = (
            NodeInputCategory.BATCH_INPUT_PARAMETER
            if predecessor_node_data.is_batch_oriented()
            else NodeInputCategory.NON_BATCH_INPUT_PARAMETER
        )
        return DynamicStepInputDefinition(
            parameter_specification=ParameterSpecification(
                parameter_name=manifest_property_name,
            ),
            category=category,
            data_lineage=predecessor_node_data.data_lineage,
            selector=predecessor_node_data.selector,
        )
    if is_step_node(execution_graph=execution_graph, node=predecessor_selector):
        predecessor_node_data = node_as(
            execution_graph=execution_graph,
            node=matching_predecessors_data[0].predecessor_selector,
            expected_type=StepNode,
        )
        category = (
            NodeInputCategory.BATCH_STEP_OUTPUT
            if predecessor_node_data.output_dimensionality > 0
            else NodeInputCategory.NON_BATCH_STEP_OUTPUT
        )
        return DynamicStepInputDefinition(
            parameter_specification=ParameterSpecification(
                parameter_name=manifest_property_name,
            ),
            category=category,
            data_lineage=predecessor_node_data.data_lineage,
            selector=predecessor_parsed_selector.value,
        )
    raise AssumptionError(
        public_message=f"Workflow Compiler for property `{manifest_property_name}` of step `{step_node}` "
        f"found data providing predecessor `{predecessor_selector}` which has unsupported step type."
        f"This is most likely the bug. "
        f"Contact Roboflow team through github issues "
        f"(https://github.com/roboflow/inference/issues) providing full "
        f"context of the problem - including workflow definition you use.",
        context="workflow_compilation | execution_graph_construction | collecting_step_inputs",
    )


def build_nested_dictionary_for_input_property(
    step_node: str,
    manifest_property_name: str,
    manifest_property_value: dict,
    predecessors_definitions: List[PropertyPredecessorDefinition],
    execution_graph: DiGraph,
) -> DictOfStepInputDefinitions:
    nested_property_name2data: Dict[Optional[str], PropertyPredecessorDefinition] = {
        predecessor_definition.parsed_selector.key: predecessor_definition
        for predecessor_definition in predecessors_definitions
    }
    result = {}
    for nested_dict_key, nested_dict_value in manifest_property_value.items():
        if nested_dict_key not in nested_property_name2data:
            result[nested_dict_key] = StaticStepInputDefinition(
                parameter_specification=ParameterSpecification(
                    parameter_name=manifest_property_name,
                    nested_element_key=nested_dict_key,
                ),
                category=NodeInputCategory.STATIC_VALUE,
                value=nested_dict_value,
            )
            continue
        referred_node_selector = nested_property_name2data[
            nested_dict_key
        ].predecessor_selector
        referred_node_parsed_selector = nested_property_name2data[
            nested_dict_key
        ].parsed_selector
        if is_input_node(execution_graph=execution_graph, node=referred_node_selector):
            predecessor_node_data = node_as(
                execution_graph=execution_graph,
                node=referred_node_selector,
                expected_type=InputNode,
            )
            category = (
                NodeInputCategory.BATCH_INPUT_PARAMETER
                if predecessor_node_data.is_batch_oriented()
                else NodeInputCategory.NON_BATCH_INPUT_PARAMETER
            )
            result[nested_dict_key] = DynamicStepInputDefinition(
                parameter_specification=ParameterSpecification(
                    parameter_name=manifest_property_name,
                    nested_element_key=nested_dict_key,
                ),
                category=category,
                data_lineage=predecessor_node_data.data_lineage,
                selector=predecessor_node_data.selector,
            )
            continue
        if is_step_node(execution_graph=execution_graph, node=referred_node_selector):
            predecessor_node_data = node_as(
                execution_graph=execution_graph,
                node=referred_node_selector,
                expected_type=StepNode,
            )
            category = (
                NodeInputCategory.BATCH_STEP_OUTPUT
                if predecessor_node_data.output_dimensionality > 0
                else NodeInputCategory.NON_BATCH_STEP_OUTPUT
            )
            result[nested_dict_key] = DynamicStepInputDefinition(
                parameter_specification=ParameterSpecification(
                    parameter_name=manifest_property_name,
                    nested_element_key=nested_dict_key,
                ),
                category=category,
                data_lineage=predecessor_node_data.data_lineage,
                selector=referred_node_parsed_selector.value,
            )
            continue
        raise AssumptionError(
            public_message=f"Workflow Compiler for property `{manifest_property_name}` of step `{step_node}` "
            f"found data providing predecessor `{referred_node_selector}` which has "
            f"unsupported step type (key: {nested_dict_key} of nested data dictionary). "
            f"This is most likely the bug. Contact Roboflow team through github issues "
            f"(https://github.com/roboflow/inference/issues) providing full "
            f"context of the problem - including workflow definition you use.",
            context="workflow_compilation | execution_graph_construction | collecting_step_inputs",
        )
    return DictOfStepInputDefinitions(
        name=manifest_property_name,
        nested_definitions=result,
    )


def get_manifest_fields_values(step_manifest: WorkflowBlockManifest) -> Dict[str, Any]:
    result = {}
    for field in step_manifest.model_fields:
        if field in EXCLUDED_FIELDS:
            continue
        result[field] = getattr(step_manifest, field)
    return result


def build_nested_list_for_input_property(
    step_node: str,
    manifest_property_name: str,
    manifest_property_value: list,
    predecessors_definitions: List[PropertyPredecessorDefinition],
    execution_graph: DiGraph,
) -> ListOfStepInputDefinitions:
    nested_index2data: Dict[Optional[int], PropertyPredecessorDefinition] = {
        e.parsed_selector.index: e for e in predecessors_definitions
    }
    result = []
    for index, element in enumerate(manifest_property_value):
        if index not in nested_index2data:
            result.append(
                StaticStepInputDefinition(
                    parameter_specification=ParameterSpecification(
                        parameter_name=manifest_property_name,
                        nested_element_index=index,
                    ),
                    category=NodeInputCategory.STATIC_VALUE,
                    value=element,
                )
            )
            continue
        referred_node_selector = nested_index2data[index].predecessor_selector
        referred_node_parsed_selector = nested_index2data[index].parsed_selector
        if is_input_node(execution_graph=execution_graph, node=referred_node_selector):
            predecessor_node_data = node_as(
                execution_graph=execution_graph,
                node=referred_node_selector,
                expected_type=InputNode,
            )
            category = (
                NodeInputCategory.BATCH_INPUT_PARAMETER
                if predecessor_node_data.is_batch_oriented()
                else NodeInputCategory.NON_BATCH_INPUT_PARAMETER
            )
            result.append(
                DynamicStepInputDefinition(
                    parameter_specification=ParameterSpecification(
                        parameter_name=manifest_property_name,
                        nested_element_index=index,
                    ),
                    category=category,
                    data_lineage=predecessor_node_data.data_lineage,
                    selector=predecessor_node_data.selector,
                )
            )
            continue
        if is_step_node(execution_graph=execution_graph, node=referred_node_selector):
            predecessor_node_data = node_as(
                execution_graph=execution_graph,
                node=referred_node_selector,
                expected_type=StepNode,
            )
            category = (
                NodeInputCategory.BATCH_STEP_OUTPUT
                if predecessor_node_data.output_dimensionality > 0
                else NodeInputCategory.NON_BATCH_STEP_OUTPUT
            )
            result.append(
                DynamicStepInputDefinition(
                    parameter_specification=ParameterSpecification(
                        parameter_name=manifest_property_name,
                        nested_element_index=index,
                    ),
                    category=category,
                    data_lineage=predecessor_node_data.data_lineage,
                    selector=referred_node_parsed_selector.value,
                )
            )
            continue
        raise AssumptionError(
            public_message=f"Workflow Compiler for property `{manifest_property_name}` of step `{step_node}` "
            f"found data providing predecessor `{referred_node_selector}` which has "
            f"unsupported step type (index: {index} of nested data list). "
            f"This is most likely the bug. Contact Roboflow team through github issues "
            f"(https://github.com/roboflow/inference/issues) providing full "
            f"context of the problem - including workflow definition you use.",
            context="workflow_compilation | execution_graph_construction | collecting_step_inputs",
        )
    return ListOfStepInputDefinitions(
        name=manifest_property_name,
        nested_definitions=result,
    )


def verify_step_input_dimensionality_offsets(
    step_name: str,
    input_dimensionality_offsets: Dict[str, int],
) -> None:
    min_offset, max_offset = None, None
    for offset in input_dimensionality_offsets.values():
        if min_offset is None or offset < min_offset:
            min_offset = offset
        if max_offset is None or offset > max_offset:
            max_offset = offset
    if min_offset is None or max_offset is None:
        return None
    if min_offset < 0 or max_offset < 0:
        raise BlockInterfaceError(
            public_message=f"Offsets could not be negative, but block defining step: {step_name} defines that values.",
            context="workflow_compilation | execution_graph_construction | verification_of_input_offset_definitions",
        )
    if abs(max_offset - min_offset) > 1:
        raise BlockInterfaceError(
            public_message="Offsets of input parameters could not differ more than 1, but block defining step {step_name} "
            f"violates that rule.",
            context="workflow_compilation | execution_graph_construction | verification_of_input_offset_definitions",
        )


def verify_output_offset(
    step_name: str,
    parameters_with_batch_inputs: Set[str],
    input_dimensionality_offsets: Dict[str, int],
    dimensionality_reference_property: Optional[str],
    output_dimensionality_offset: int,
) -> None:
    if not parameters_with_batch_inputs and output_dimensionality_offset < 0:
        raise StepInputDimensionalityError(
            public_message=f"Block defining step {step_name} defines negative dimensionality offset while only "
            f"scalar inputs being provided - the block cannot run as there is no dimension to collapse.",
            context="workflow_compilation | execution_graph_construction | verification_of_output_offset",
        )
    if (
        dimensionality_reference_property is not None
        and dimensionality_reference_property not in parameters_with_batch_inputs
    ):
        raise BlockInterfaceError(
            public_message=f"Block defining step {step_name} defines dimensionality reference property "
            f"which is not in scope of parameters bring batch-oriented input, which makes it impossible "
            f"to use as reference for output dimensionality.",
            context="workflow_compilation | execution_graph_construction | verification_of_output_offset",
        )
    if output_dimensionality_offset not in {-1, 0, 1}:
        raise BlockInterfaceError(
            public_message=f"Block defining step {step_name} defines output dimensionality offset "
            f"being {output_dimensionality_offset}, whereas it is only possible for that offset being "
            f"in set [-1, 0, 1].",
            context="workflow_compilation | execution_graph_construction | verification_of_output_offset",
        )
    different_offsets = {o for o in input_dimensionality_offsets.values()}
    if len(parameters_with_batch_inputs) != len(input_dimensionality_offsets):
        # assumption of default offset being zero - and this one does not need to be manifested
        # by block
        different_offsets.add(0)
    if 0 not in different_offsets and parameters_with_batch_inputs:
        raise BlockInterfaceError(
            public_message=f"Block defining step {step_name} explicitly defines input dimensionalities offsets"
            f"with {input_dimensionality_offsets}, but the definition lack 0-level input, which is "
            f"not allowed, as in this scenario offsets could be adjusted to include 0",
            context="workflow_compilation | execution_graph_construction | verification_of_output_offset",
        )
    if len(different_offsets) > 1 and dimensionality_reference_property is None:
        raise BlockInterfaceError(
            public_message=f"Block defining step {step_name} explicitly defines input dimensionality "
            f"offsets {input_dimensionality_offsets}. In this scenario it is required to provide dimensionality "
            f"reference property.",
            context="workflow_compilation | execution_graph_construction | verification_of_output_offset",
        )
    if len(different_offsets) > 1 and output_dimensionality_offset != 0:
        raise BlockInterfaceError(
            public_message=f"Block defining step {step_name} explicitly defines input dimensionality "
            f"offsets {input_dimensionality_offsets} and output dimensionality offset {output_dimensionality_offset} "
            f"where the latter is not 0, but for inputs differing with dimensionality it is only possible to keep "
            f"output dimensionality the same and point reference parameter.",
            context="workflow_compilation | execution_graph_construction | verification_of_output_offset",
        )


def verify_input_data_dimensionality(
    step_name: str,
    step_type: str,
    dimensionality_reference_property: Optional[str],
    inputs_dimensionalities: Dict[str, Set[int]],
    dimensionality_offstes: Dict[str, int],
) -> None:
    parameter_name2dimensionality_specification = (
        grab_input_data_dimensionality_specifications(
            step_name=step_name,
            inputs_dimensionalities=inputs_dimensionalities,
            dimensionality_offstes=dimensionality_offstes,
        )
    )
    if not parameter_name2dimensionality_specification:
        # nothing to check if no batch-oriented inputs
        return None
    different_dimensionalities_of_parameters = {
        specification.actual_dimensionality
        for specification in parameter_name2dimensionality_specification.values()
    }
    if dimensionality_reference_property is None:
        if len(different_dimensionalities_of_parameters) > 1:
            parameter2dimensionality = {
                k: e.actual_dimensionality
                for k, e in parameter_name2dimensionality_specification.items()
            }
            raise StepInputDimensionalityError(
                public_message=f"Block defining step {step_name} does not define dimensionality reference property, "
                f"which means that all batch-oriented parameters must be at the same dimensionality level, "
                f"but detected the following dimensionalities for parameters {parameter2dimensionality}",
                context="workflow_compilation | execution_graph_construction | denoting_step_inputs_dimensionality",
                blocks_errors=[
                    WorkflowBlockError(
                        block_id=step_name,
                        block_type=step_type,
                        block_details=f"Dimensionality of input parameters doesn't match: {parameter2dimensionality}",
                    )
                ],
            )
        return None
    reference_specification = parameter_name2dimensionality_specification[
        dimensionality_reference_property
    ]
    expected_dimensionalities = {
        property_name: (e.expected_offset - reference_specification.expected_offset)
        + reference_specification.actual_dimensionality
        for property_name, e in parameter_name2dimensionality_specification.items()
    }
    if any(dim <= 0 for dim in expected_dimensionalities.values()):
        raise StepInputDimensionalityError(
            public_message=f"Given the definition of block defining step {step_name} and data provided, "
            f"the block would expect batch input dimensionality to be 0 or below, which is invalid.",
            context="workflow_compilation | execution_graph_construction | denoting_step_inputs_dimensionality",
        )
    for property_name, expected_dimensionality in expected_dimensionalities.items():
        actual_dimensionality = parameter_name2dimensionality_specification[
            property_name
        ].actual_dimensionality
        if actual_dimensionality != expected_dimensionality:
            raise StepInputDimensionalityError(
                public_message=f"Data fed into step `{step_name}` property `{property_name}` has "
                f"actual dimensionality {actual_dimensionality}, "
                f"when expected was {expected_dimensionality}",
                context="workflow_compilation | execution_graph_construction | denoting_step_inputs_dimensionality",
                blocks_errors=[
                    WorkflowBlockError(
                        block_id=step_name,
                        block_type=step_type,
                        property_name=property_name,
                        property_details=f"Expected dimensionality {expected_dimensionality}, but received dimensionality {actual_dimensionality}",
                    )
                ],
            )
    return None


def grab_input_data_dimensionality_specifications(
    step_name: str,
    inputs_dimensionalities: Dict[str, Set[int]],
    dimensionality_offstes: Dict[str, int],
) -> Dict[str, InputDimensionalitySpecification]:
    result = {}
    for parameter_name, dimensionality in inputs_dimensionalities.items():
        parameter_offset = dimensionality_offstes.get(parameter_name, 0)
        non_zero_dimensionalities_for_parameter = {d for d in dimensionality if d > 0}
        if len(non_zero_dimensionalities_for_parameter) > 1:
            raise AssumptionError(
                public_message=f"Workflow Compiler for step: `{step_name}` and parameter: {parameter_name}"
                f"found multiple different values of actual input dimensionalities: "
                f"`{non_zero_dimensionalities_for_parameter}` which should be detected and addresses "
                f"at earlier stages of compilation."
                f"This is most likely the bug. Contact Roboflow team through github issues "
                f"(https://github.com/roboflow/inference/issues) providing full "
                f"context of the problem - including workflow definition you use.",
                context="workflow_compilation | execution_graph_construction | collecting_step_inputs",
            )
        if non_zero_dimensionalities_for_parameter:
            actual_dimensionality = next(iter(non_zero_dimensionalities_for_parameter))
            result[parameter_name] = InputDimensionalitySpecification(
                actual_dimensionality=actual_dimensionality,
                expected_offset=parameter_offset,
            )
    return result


def verify_compatibility_of_input_data_lineage_with_control_flow_lineage(
    step_name: str,
    inputs_lineage: List[List[str]],
    control_flow_steps_selectors: List[str],
    execution_graph: DiGraph,
) -> None:
    """
    Ensure control flow steps' data lineage is compatible with the step's inputs.

    Control flow steps that affect this step must operate on data that is
    compatible with the data fed to the step; otherwise the step could never
    execute. Compares unique control flow lineages against input lineage
    prefixes and raises ControlFlowDefinitionError if any control flow lineage
    is not covered by the inputs.

    If inputs_lineage is empty, there is no sense to verify compatibility. The lineage of the
    step should be established based on the control flow lineages.

    Args:
        step_name: Name of the step being verified (used in error messages).
        inputs_lineage: Data lineages derived from the step's input data.
        control_flow_steps_selectors: Step selectors of control flow steps
            that affect this step's execution.
        execution_graph: The workflow execution graph.

    Raises:
        ControlFlowDefinitionError: When a control flow step's lineage is not
            compatible with the step's input lineage (step would never execute).
    """
    (
        batch_oriented_control_flow_lineages,
        lineage_id2control_flow_steps,
    ) = _collect_unique_control_flow_lineages_with_step_mapping(
        control_flow_steps_selectors=control_flow_steps_selectors,
        execution_graph=execution_graph,
    )
    if not inputs_lineage:
        return

    all_input_lineage_prefixes = get_all_batch_lineage_prefixes(lineages=inputs_lineage)
    all_input_lineage_prefixes_hashes = {
        identify_lineage(lineage=lineage) for lineage in all_input_lineage_prefixes
    }
    for control_flow_lineage in batch_oriented_control_flow_lineages:
        control_flow_lineage_id = identify_lineage(lineage=control_flow_lineage)
        if control_flow_lineage_id not in all_input_lineage_prefixes_hashes:
            problematic_flow_control_steps = lineage_id2control_flow_steps[
                control_flow_lineage_id
            ]
            raise ControlFlowDefinitionError(
                public_message=f"Step {step_name} execution is impacted by control flow outcome of the following "
                f"steps {problematic_flow_control_steps} which make decision based on data that is "
                f"not compatible with data fed to the step {step_name} - which would cause the step "
                f"to never execute. This behaviour is invalid and prevented upfront by Workflows compiler.",
                context="workflow_compilation | execution_graph_construction | verification_of_control_flow_lineage",
            )


def get_all_batch_lineage_prefixes(lineages: List[List[str]]) -> List[List[str]]:
    result = []
    already_spotted = set()
    for lineage in lineages:
        lineage_prefixes = get_batch_lineage_prefixes(lineage=lineage)
        for lineage_prefix in lineage_prefixes:
            lineage_prefix_id = identify_lineage(lineage=lineage_prefix)
            if lineage_prefix_id not in already_spotted:
                already_spotted.add(lineage_prefix_id)
                result.append(lineage_prefix)
    return result


def get_batch_lineage_prefixes(lineage: List[str]) -> List[List[str]]:
    if not lineage:
        return []
    result = []
    prefix = []
    for i in range(len(lineage)):
        prefix.append(lineage[i])
        result.append(copy(prefix))
    return result


def get_inputs_dimensionalities(
    step_name: str,
    step_type: str,
    input_data: StepInputData,
    scalar_parameters_to_be_batched: Set[str],
    input_dimensionality_offsets: Dict[str, int],
) -> Dict[str, Set[int]]:
    result = defaultdict(set)
    dimensionalities_spotted = set()
    offset_parameters = {
        parameter: value
        for parameter, value in input_dimensionality_offsets.items()
        if value > 0
    }
    non_offset_parameters_dimensionality_values = set()
    for property_name, input_definition in input_data.items():
        if property_name in offset_parameters:
            continue
        if input_definition.is_compound_input():
            for value in input_definition.iterate_through_definitions():
                if value.is_batch_oriented():
                    non_offset_parameters_dimensionality_values.add(
                        value.get_dimensionality()
                    )
        elif input_definition.is_batch_oriented():
            non_offset_parameters_dimensionality_values.add(
                input_definition.get_dimensionality()
            )
    if len(non_offset_parameters_dimensionality_values) > 1:
        raise StepInputDimensionalityError(
            public_message=f"For step {step_name} attempted to plug input data that are in different dimensions, "
            f"whereas block defines the inputs to be equal in that terms.",
            context="workflow_compilation | execution_graph_construction | collecting_step_input_data",
            blocks_errors=[
                WorkflowBlockError(
                    block_id=step_name,
                    block_type=step_type,
                    block_details=f"Dimensionality of input parameters differs by more than 1. Detected dimensions: {dict(result)}",
                )
            ],
        )
    non_offset_parameters_dimensionality_value = (
        non_offset_parameters_dimensionality_values.pop()
        if len(non_offset_parameters_dimensionality_values) > 0
        else 1
    )
    for property_name, input_definition in input_data.items():
        if input_definition.is_compound_input():
            result[property_name] = get_compound_input_dimensionality(
                step_name=step_name,
                step_type=step_type,
                property_name=property_name,
                input_definition=input_definition,
                scalar_parameters_to_be_batched=scalar_parameters_to_be_batched,
                non_offset_parameters_dimensionality_value=non_offset_parameters_dimensionality_value,
                offset_parameters=offset_parameters,
            )
        else:
            if property_name in scalar_parameters_to_be_batched:
                if property_name not in offset_parameters:
                    result[property_name] = {non_offset_parameters_dimensionality_value}
                else:
                    result[property_name] = (
                        non_offset_parameters_dimensionality_value
                        + offset_parameters[property_name]
                    )
            else:
                result[property_name] = {input_definition.get_dimensionality()}
        dimensionalities_spotted.update(result[property_name])
    non_zero_dimensionalities_spotted = {d for d in dimensionalities_spotted if d != 0}
    if len(non_zero_dimensionalities_spotted) > 0:
        min_dim, max_dim = min(non_zero_dimensionalities_spotted), max(
            non_zero_dimensionalities_spotted
        )
        if abs(max_dim - min_dim) > 1:
            raise StepInputDimensionalityError(
                public_message=f"For step {step_name} attempted to plug input data differing in dimensionality more than 1",
                context="workflow_compilation | execution_graph_construction | collecting_step_input_data",
                blocks_errors=[
                    WorkflowBlockError(
                        block_id=step_name,
                        block_type=step_type,
                        block_details=f"Dimensionality of input parameters differs by more than 1. Detected dimensions: {dict(result)}",
                    )
                ],
            )
    return result


def get_compound_input_dimensionality(
    step_name: str,
    property_name: str,
    step_type: str,
    input_definition: CompoundStepInputDefinition,
    scalar_parameters_to_be_batched: Set[str],
    offset_parameters: Dict[str, int],
    non_offset_parameters_dimensionality_value: int,
) -> Set[int]:
    dimensionalities_spotted = set()
    for definition in input_definition.iterate_through_definitions():
        if (
            property_name not in scalar_parameters_to_be_batched
            or definition.is_batch_oriented()
        ):
            dimensionalities_spotted.add(definition.get_dimensionality())
        elif property_name not in offset_parameters:
            dimensionalities_spotted.add(non_offset_parameters_dimensionality_value)
        else:
            dimensionalities_spotted.add(
                non_offset_parameters_dimensionality_value
                + offset_parameters[property_name]
            )
    non_zero_dimensionalities = {e for e in dimensionalities_spotted if e != 0}
    if len(non_zero_dimensionalities) > 1:
        raise StepInputDimensionalityError(
            public_message=f"While evaluating compound property {property_name} of step {step_name}, "
            f"detected multiple inputs of differing batch dimensionalities: {non_zero_dimensionalities}",
            context="workflow_compilation | execution_graph_construction | collecting_step_input_data",
            blocks_errors=[
                WorkflowBlockError(
                    block_id=step_name,
                    block_type=step_type,
                    property_name=property_name,
                    property_details=f"Detected multiple inputs of differing batch dimensionalities: {non_zero_dimensionalities}",
                )
            ],
        )
    return dimensionalities_spotted


def grab_parameters_defining_batch_inputs(
    inputs_dimensionalities: Dict[str, Set[int]],
) -> Set[str]:
    result = set()
    for paremeter, dimensionalities in inputs_dimensionalities.items():
        batch_dimensionalities = {d for d in dimensionalities if d > 0}
        if len(batch_dimensionalities):
            result.add(paremeter)
    return result


def retrieve_batch_compatibility_of_input_selectors(
    input_selectors: List[ParsedSelector],
) -> Dict[str, Set[bool]]:
    batch_compatibility_of_properties = defaultdict(set)
    for parsed_selector in input_selectors:
        for reference in parsed_selector.definition.allowed_references:
            batch_compatibility_of_properties[
                parsed_selector.definition.property_name
            ].update(reference.points_to_batch)
    return batch_compatibility_of_properties


def verify_declared_batch_compatibility_against_actual_inputs(
    node: str,
    step_node_data: StepNode,
    input_data: StepInputData,
    batch_compatibility_of_properties: Dict[str, Set[bool]],
) -> Set[str]:
    scalar_parameters_to_be_batched = set()
    parameters_accepting_batches_and_scalars = set(
        step_node_data.step_manifest.get_parameters_accepting_batches_and_scalars()
    )
    hardcoded_inputs_to_be_batch_compatible = set(
        step_node_data.step_manifest.get_parameters_enforcing_auto_batch_casting()
        + step_node_data.step_manifest.get_parameters_accepting_batches()
    )
    for property_name, input_definition in input_data.items():
        if property_name not in batch_compatibility_of_properties:
            actual_input_is_batch = {False}
            if property_name in parameters_accepting_batches_and_scalars:
                batch_compatibility = {True, False}
            elif property_name in hardcoded_inputs_to_be_batch_compatible:
                batch_compatibility = {True}
            else:
                continue
        elif input_definition.is_compound_input():
            actual_input_is_batch = {
                element.is_batch_oriented()
                for element in input_definition.iterate_through_definitions()
            }
            batch_compatibility = batch_compatibility_of_properties[property_name]
        else:
            actual_input_is_batch = {input_definition.is_batch_oriented()}
            batch_compatibility = batch_compatibility_of_properties[property_name]
        step_accepts_batch_input = step_node_data.step_manifest.accepts_batch_input()
        if (
            step_accepts_batch_input
            and batch_compatibility == {False}
            and True in actual_input_is_batch
        ):
            raise ExecutionGraphStructureError(
                public_message=f"Detected invalid reference plugged "
                f"into property `{property_name}` of step `{node}` - the step "
                f"property do not accept batch-oriented inputs, yet the input selector "
                f"holds one - this indicates the problem with "
                f"construction of your Workflow - usually the problem occurs when non-batch oriented "
                f"step inputs are filled with outputs of batch-oriented steps or batch-oriented inputs.",
                context="workflow_compilation | execution_graph_construction",
            )
        if batch_compatibility == {True} and False in actual_input_is_batch:
            scalar_parameters_to_be_batched.add(property_name)
    return scalar_parameters_to_be_batched


def get_input_data_lineage_excluding_auto_batch_casting(
    step_name: str,
    input_data: StepInputData,
    scalar_parameters_to_be_batched: Set[str],
) -> List[List[str]]:
    lineage_deduplication_set = set()
    lineages = []
    for property_name, input_definition in input_data.items():
        new_lineages_detected_within_property_data = get_lineage_for_input_property(
            step_name=step_name,
            property_name=property_name,
            input_definition=input_definition,
            lineage_deduplication_set=lineage_deduplication_set,
        )
        if (
            property_name in scalar_parameters_to_be_batched
            and len(new_lineages_detected_within_property_data) == 0
        ):
            continue
        lineages.extend(new_lineages_detected_within_property_data)
    if not lineages:
        return lineages
    verify_lineages(step_name=step_name, detected_lineages=lineages)
    return lineages


def get_lineage_support_for_auto_batch_casted_parameters(
    input_dimensionalities: Dict[str, Set[int]],
    scalar_parameters_to_be_batched: Set[str],
    all_lineages_of_batch_parameters: List[List[str]],
) -> Dict[str, AutoBatchCastingConfig]:
    longest_lineage_support = find_longest_lineage_support(
        all_lineages_of_batch_parameters=all_lineages_of_batch_parameters,
    )
    result = {}
    for parameter_name in scalar_parameters_to_be_batched:
        parameter_dimensionality = max(input_dimensionalities[parameter_name])
        if longest_lineage_support is None:
            lineage_support = None
        else:
            lineage_support = longest_lineage_support[:parameter_dimensionality]
        result[parameter_name] = AutoBatchCastingConfig(
            casted_dimensionality=parameter_dimensionality,
            lineage_support=lineage_support,
        )
    return result


def find_longest_lineage_support(
    all_lineages_of_batch_parameters: List[List[str]],
) -> Optional[List[str]]:
    longest_longest_lineage_support = []
    for lineage in all_lineages_of_batch_parameters:
        if len(lineage) > len(longest_longest_lineage_support):
            longest_longest_lineage_support = lineage
    if len(longest_longest_lineage_support) == 0:
        return None
    return longest_longest_lineage_support


def get_lineage_for_input_property(
    step_name: str,
    property_name: str,
    input_definition: Union[StepInputDefinition, CompoundStepInputDefinition],
    lineage_deduplication_set: Set[int],
) -> List[List[str]]:
    if input_definition.is_compound_input():
        return get_lineage_from_compound_input(
            step_name=step_name,
            property_name=property_name,
            input_definition=input_definition,
            lineage_deduplication_set=lineage_deduplication_set,
        )
    lineages = []
    if input_definition.is_batch_oriented():
        lineage = input_definition.data_lineage
        lineage_id = identify_lineage(lineage=lineage)
        if lineage_id not in lineage_deduplication_set:
            lineage_deduplication_set.add(lineage_id)
            lineages.append(lineage)
    return lineages


def get_lineage_from_compound_input(
    step_name: str,
    property_name: str,
    input_definition: Union[StepInputDefinition, CompoundStepInputDefinition],
    lineage_deduplication_set: Set[int],
) -> List[List[str]]:
    lineages = []
    for nested_element in input_definition.iterate_through_definitions():
        if nested_element.is_batch_oriented():
            lineage = nested_element.data_lineage
            lineage_id = identify_lineage(lineage=lineage)
            if lineage_id not in lineage_deduplication_set:
                lineage_deduplication_set.add(lineage_id)
                lineages.append(lineage)
    if len(lineages) > 1:
        raise StepInputLineageError(
            public_message=f"Input data provided for step: `{step_name}` comes with multiple different lineages "
            f"{lineages} for property `{property_name}` accepting "
            f"multiple selectors, which is not allowed.",
            context="workflow_compilation | execution_graph_construction | collecting_step_inputs_lineage",
        )
    return lineages


def verify_lineages(step_name: str, detected_lineages: List[List[str]]) -> None:
    lineages_by_length = defaultdict(list)
    for lineage in detected_lineages:
        lineages_by_length[len(lineage)].append(lineage)
    if len(lineages_by_length) > 2:
        raise StepInputLineageError(
            public_message=f"Input data provided for step: `{step_name}` comes with lineages at more than two "
            f"dimensionality levels, which should not be possible.",
            context="workflow_compilation | execution_graph_construction | collecting_step_inputs_lineage",
        )
    lineage_lengths = sorted(lineages_by_length.keys())
    for lineage_length in lineage_lengths:
        if len(lineages_by_length[lineage_length]) > 1:
            raise StepInputLineageError(
                public_message=f"Among step `{step_name}` inputs found different lineages at the same  "
                f"dimensionality levels dimensionality.",
                context="workflow_compilation | execution_graph_construction | collecting_step_inputs_lineage",
            )
    if len(lineages_by_length[lineage_lengths[-1]]) > 1 and len(lineage_lengths) < 2:
        raise StepInputLineageError(
            public_message=f"If lineage differ at the last level, then Execution Engine requires at least one "
            f"higher-dimension  input that will come with common lineage, but this is not the "
            f"case for step {step_name}.",
            context="workflow_compilation | execution_graph_construction | collecting_step_inputs_lineage",
        )
    if len(lineage_lengths) == 2:
        reference_lineage = lineages_by_length[lineage_lengths[0]][0]
        if reference_lineage != lineages_by_length[lineage_lengths[1]][0][:-1]:
            raise StepInputLineageError(
                public_message=f"Step `{step_name}` inputs does not share common lineage. Differing element: "
                f"{lineages_by_length[lineage_lengths[1]][0]}, "
                f"reference lineage prefix: {reference_lineage}",
                context="workflow_compilation | execution_graph_construction | collecting_step_inputs_lineage",
            )


def establish_step_execution_dimensionality(
    inputs_dimensionalities: Dict[str, Set[int]],
    control_flow_lineage_support: List[str],
    output_dimensionality_offset: int,
) -> int:
    """
    Determine how many batch dimensions (execution slices) a step runs with.

    Used during workflow compilation in denote_data_flow_for_step. The result
    is stored on StepNode.step_execution_dimensionality and consumed at
    execution time to:
    - Drive how many times the step is executed (which batch indices/slices).
    - Align and expand inputs (e.g. auto-batch casting) to match this size.
    - Validate that parameter dimensionalities are compatible (runtime checks
      in step_input_assembler and manager).

    Logic:
    - If no input has non-zero dimensionality but the step is gated by
      control flow (control_flow_lineage_support non-empty), the
      dimensionality is the number of control-flow branches.
    - Otherwise, the minimum non-zero input dimensionality is used; if
      output_dimensionality_offset < 0 (step reduces batch dimension),
      one is subtracted.

    Args:
        inputs_dimensionalities: Per-input sets of dimensionalities (from
            get_inputs_dimensionalities).
        control_flow_lineage_support: Lineage identifiers for control-flow
            branches that gate this step (from establish_batch_oriented_step_lineage).
        output_dimensionality_offset: Block's output dimensionality offset
            (positive = expand, negative = reduce batch dimension).

    Returns:
        The number of batch dimensions (execution slices) for this step.
    """
    step_execution_dimensionality = 0
    non_zero_dimensionalities = {
        dimensionality
        for dimensionalities in inputs_dimensionalities.values()
        for dimensionality in dimensionalities
        if dimensionality > 0
    }
    if len(non_zero_dimensionalities) == 0 and len(control_flow_lineage_support) > 0:
        return len(control_flow_lineage_support)
    if len(non_zero_dimensionalities) > 0:
        step_execution_dimensionality = min(non_zero_dimensionalities)
        if output_dimensionality_offset < 0:
            step_execution_dimensionality -= 1
    return step_execution_dimensionality


def establish_batch_oriented_step_lineage(
    step_selector: str,
    all_data_derived_lineages: List[List[str]],
    all_control_flow_lineages: List[List[str]],
    input_data: StepInputData,
    dimensionality_reference_property: Optional[str],
    output_dimensionality_offset: int,
) -> Tuple[List[str], List[str]]:
    reference_lineage, control_flow_lineage_support = get_reference_lineage(
        step_selector=step_selector,
        all_data_derived_lineages=all_data_derived_lineages,
        all_control_flow_lineages=all_control_flow_lineages,
        input_data=input_data,
        dimensionality_reference_property=dimensionality_reference_property,
    )
    if output_dimensionality_offset < 0:
        result_dimensionality = reference_lineage[:output_dimensionality_offset]
        return result_dimensionality, []
    if output_dimensionality_offset == 0:
        return reference_lineage, control_flow_lineage_support
    reference_lineage.append(step_selector)
    return reference_lineage, []


def get_reference_lineage(
    step_selector: str,
    all_data_derived_lineages: List[List[str]],
    all_control_flow_lineages: List[List[str]],
    input_data: StepInputData,
    dimensionality_reference_property: Optional[str],
) -> Tuple[List[str], List[str]]:
    if not all_data_derived_lineages:
        if len(all_control_flow_lineages) == 1:
            return copy(all_control_flow_lineages[0]), copy(
                all_control_flow_lineages[0]
            )
        # Multiple control-flow lineages and no data-derived lineage: pick the deepest.
        max_lineage_len = max(len(lineage) for lineage in all_control_flow_lineages)
        lineages_matching_max_len = [
            _lineage
            for _lineage in all_control_flow_lineages
            if len(_lineage) == max_lineage_len
        ]
        if len(lineages_matching_max_len) == 1:
            return copy(lineages_matching_max_len[0]), copy(
                lineages_matching_max_len[0]
            )
        else:
            raise AssumptionError(
                public_message=f"Multiple control-flow lineages at the same level for step `{step_selector}` "
                f"and no data-derived lineage; unique lineage level required. "
                f"Contact Roboflow team through github issues "
                f"(https://github.com/roboflow/inference/issues) providing full "
                f"context of the problem - including workflow definition you use.",
                context="workflow_compilation | execution_graph_construction | collecting_step_inputs_lineage",
            )
    if len(all_data_derived_lineages) == 1:
        return copy(all_data_derived_lineages[0]), []
    if dimensionality_reference_property not in input_data:
        raise AssumptionError(
            public_message=f"Workflow Compiler for step: `{step_selector}` expected dimensionality_reference_property "
            f"presence to be verified at earlier stages, which did not happen as expected. "
            f"This is most likely the bug. Contact Roboflow team through github issues "
            f"(https://github.com/roboflow/inference/issues) providing full "
            f"context of the problem - including workflow definition you use.",
            context="workflow_compilation | execution_graph_construction | collecting_step_inputs_lineage",
        )
    property_data = input_data[dimensionality_reference_property]
    if property_data.is_compound_input():
        lineage = None
        for nested_element in property_data.iterate_through_definitions():
            if nested_element.is_batch_oriented():
                lineage = copy(nested_element.data_lineage)
                return lineage, []
        if lineage is None:
            raise AssumptionError(
                public_message=f"Workflow Compiler for step: `{step_selector}` cannot establish output lineage. "
                f"At this stage it is expected to succeed - lack of success indicates bug. "
                f"Contact Roboflow team through github issues "
                f"(https://github.com/roboflow/inference/issues) providing full "
                f"context of the problem - including workflow definition you use.",
                context="workflow_compilation | execution_graph_construction | collecting_step_inputs_lineage",
            )
    if not property_data.is_batch_oriented():
        raise AssumptionError(
            public_message=f"Workflow Compiler for step: `{step_selector}` cannot establish output lineage. "
            f"At this stage it is expected to succeed - lack of success indicates bug. "
            f"Contact Roboflow team through github issues "
            f"(https://github.com/roboflow/inference/issues) providing full "
            f"context of the problem - including workflow definition you use.",
            context="workflow_compilation | execution_graph_construction | collecting_step_inputs_lineage",
        )
    return copy(property_data.data_lineage), []


def get_property_with_invalid_selector(
    manifest: WorkflowBlockManifest, step_property: str
) -> Optional[str]:
    property_name = None
    for key, value in manifest.__dict__.items():
        if isinstance(value, str) and step_property in value:
            property_name = key
            break
    return property_name


def add_super_input_node_in_execution_graph(
    execution_graph: DiGraph,
    super_input_node: str,
) -> DiGraph:
    nodes_to_attach_super_input_into = get_nodes_of_specific_category(
        execution_graph=execution_graph,
        category=NodeCategory.INPUT_NODE,
    )
    step_nodes_without_predecessors = [
        node
        for node in execution_graph.nodes
        if not list(execution_graph.predecessors(node))
    ]
    nodes_to_attach_super_input_into.update(step_nodes_without_predecessors)
    execution_graph.add_node(super_input_node)
    for node in nodes_to_attach_super_input_into:
        execution_graph.add_edge(super_input_node, node)
    return execution_graph
