/**
 * In Honor of Otávio Anovazzi (@anovazzi1)
 *
 * This file contains the highest number of commits by Otávio in the entire Langflow project,
 * reflecting his unmatched dedication, expertise, and innovative spirit. Each line of code
 * is a testament to his relentless pursuit of excellence and his significant impact on this
 * project's evolution.

 * His commitment to selflessly helping others embodies the true meaning of open source,
 * and his legacy lives on in each one of his 2771 contributions, inspiring us to build exceptional
 * software for all.
 */

// biome-ignore-all lint/suspicious/noExplicitAny: TODO We need to fix this. added suppression for 1.7.1 merge
import {
  type Connection,
  type Edge,
  getOutgoers,
  type Node,
  type OnSelectionChangeParams,
  type ReactFlowJsonObject,
  type XYPosition,
} from "@xyflow/react";
import { cloneDeep } from "lodash";
import ShortUniqueId from "short-unique-id";
import {
  getLeftHandleId,
  getRightHandleId,
} from "@/CustomNodes/utils/get-handle-id";
import i18n from "../i18n";
import { customDownloadNodeJson } from "@/customization/utils/custom-download-json";
import { customDownloadFlow } from "@/customization/utils/custom-reactFlowUtils";
import useFlowStore from "@/stores/flowStore";
import getFieldTitle from "../CustomNodes/utils/get-field-title";
import {
  INPUT_TYPES,
  IS_MAC,
  LANGFLOW_SUPPORTED_TYPES,
  OUTPUT_TYPES,
  specialCharsRegex,
} from "../constants/constants";
import { DESCRIPTIONS } from "../flow_constants";
import type {
  APIClassType,
  APIKindType,
  APIObjectType,
  APITemplateType,
  InputFieldType,
  OutputFieldType,
} from "../types/api";
import type {
  AllNodeType,
  EdgeType,
  FlowType,
  NodeDataType,
  sourceHandleType,
  targetHandleType,
} from "../types/flow";
import type {
  addEscapedHandleIdsToEdgesType,
  findLastNodeType,
  generateFlowType,
  updateEdgesHandleIdsType,
} from "../types/utils/reactflowUtils";
import {
  cleanMcpConfig,
  type MCPServerValue,
} from "./helpers/clean-mcp-config";
import { getLayoutedNodes } from "./layoutUtils";
import { createRandomKey, toTitleCase } from "./utils";

const uid = new ShortUniqueId();

export function checkChatInput(nodes: Node[]) {
  return nodes.some((node) => node.data.type === "ChatInput");
}

export function checkWebhookInput(nodes: Node[]) {
  return nodes.some((node) => node.data.type === "Webhook");
}

export function cleanEdges(nodes: AllNodeType[], edges: EdgeType[]) {
  const brokenEdges: {
    source: {
      nodeDisplayName: string;
      outputDisplayName?: string;
    };
    target: {
      displayName: string;
      field: string;
    };
  }[] = [];

  function generateAlertObject(sourceNode, targetNode, edge) {
    const targetHandleObject: targetHandleType = scapeJSONParse(
      edge.targetHandle,
    );
    const sourceHandleObject: sourceHandleType = scapeJSONParse(
      edge.sourceHandle,
    );
    const name = sourceHandleObject.name;
    const output = sourceNode.data.node!.outputs?.find(
      (output) => output.name === name,
    );

    return {
      source: {
        nodeDisplayName: sourceNode.data.node!.display_name,
        outputDisplayName: output?.display_name,
      },
      target: {
        displayName: targetNode.data.node!.display_name,
        field:
          targetNode.data.node!.template[targetHandleObject.fieldName]
            ?.display_name ??
          targetHandleObject.fieldName ??
          targetHandleObject.name,
      },
    };
  }

  let newEdges: EdgeType[] = cloneDeep(
    edges.map((edge) => ({ ...edge, selected: false, animated: false })),
  );
  edges.forEach((edge) => {
    // check if the source and target node still exists
    const sourceNode = nodes.find((node) => node.id === edge.source);
    const targetNode = nodes.find((node) => node.id === edge.target);
    if (!sourceNode || !targetNode) {
      newEdges = newEdges.filter((edg) => edg.id !== edge.id);
      return;
    }
    // check if the source and target handle still exists
    const sourceHandle = edge.sourceHandle; //right
    const targetHandle = edge.targetHandle; //left
    if (targetHandle) {
      const targetHandleObject: targetHandleType = scapeJSONParse(targetHandle);
      const field = targetHandleObject.fieldName;
      let id: targetHandleType | sourceHandleType;

      const templateFieldType = targetNode.data.node!.template[field]?.type;
      const rawInputTypes = targetNode.data.node!.template[field]?.input_types;
      const modelType = targetNode.data.node!.template[field]?.model_type;
      // For ModelInput types, default based on model_type:
      // - "embedding" -> ["Embeddings"]
      // - "language" (default) -> ["LanguageModel"]
      const isModelType = templateFieldType === "model";
      const defaultModelInputType =
        modelType === "embedding" ? "Embeddings" : "LanguageModel";
      const inputTypes = rawInputTypes?.length
        ? rawInputTypes
        : isModelType
          ? [defaultModelInputType]
          : rawInputTypes;
      const hasProxy = targetNode.data.node!.template[field]?.proxy;
      const isToolMode = targetNode.data.node!.template[field]?.tool_mode;
      const isAdvanced = targetNode.data.node!.template[field]?.advanced;

      if (
        !field &&
        targetHandleObject.name &&
        targetNode.type === "genericNode"
      ) {
        const dataType = targetNode.data.type;
        const output = targetNode.data.node!.outputs?.find(
          (output) => output.name === targetHandleObject.name,
        );
        const baseTypes = output?.types ?? [];
        // Include loop_types for loop inputs (allows_loop=true)
        const outputTypes =
          output?.allows_loop && output?.loop_types
            ? [output.selected ?? baseTypes[0], ...output.loop_types]
            : baseTypes;

        id = {
          dataType: dataType ?? "",
          name: targetHandleObject.name,
          id: targetNode.data.id,
          output_types: outputTypes,
        };
      } else {
        id = {
          type: templateFieldType,
          fieldName: field,
          id: targetNode.data.id,
          inputTypes: inputTypes,
        };
        if (hasProxy) {
          id.proxy = targetNode.data.node!.template[field]?.proxy;
        }
      }
      // Check if target is an loop input (allows_loop=true)
      const targetOutput = targetNode.data.node!.outputs?.find(
        (output) => output.name === targetHandleObject.name,
      );
      const isLoopInput = targetOutput?.allows_loop === true;

      // Backward compatibility: old flows may have Data/DataFrame types that need to match JSON/Table
      const expectedTargetHandle = scapedJSONStringfy(id);
      const targetHandlesMatchResult = handlesMatch(
        expectedTargetHandle,
        targetHandle,
      );
      if (
        (!targetHandlesMatchResult ||
          (targetNode.data.node?.tool_mode && isToolMode) ||
          isAdvanced) &&
        !isLoopInput
      ) {
        newEdges = newEdges.filter((e) => e.id !== edge.id);
        brokenEdges.push(generateAlertObject(sourceNode, targetNode, edge));
      } else if (
        targetHandlesMatchResult &&
        expectedTargetHandle !== targetHandle
      ) {
        // Handles match via migration but IDs differ — update edge to use current types
        // so React Flow can find the DOM handle
        const edgeInNewEdges = newEdges.find((e) => e.id === edge.id);
        if (edgeInNewEdges) {
          edgeInNewEdges.targetHandle = expectedTargetHandle;
          if (edgeInNewEdges.data) {
            edgeInNewEdges.data.targetHandle =
              scapeJSONParse(expectedTargetHandle);
          }
          // Update edge ID to reflect new handles
          edgeInNewEdges.id =
            "reactflow__edge-" +
            edgeInNewEdges.source +
            (edgeInNewEdges.sourceHandle ?? "") +
            "-" +
            edgeInNewEdges.target +
            expectedTargetHandle;
        }
      }
    }
    if (sourceHandle) {
      const parsedSourceHandle = scapeJSONParse(sourceHandle);
      const name = parsedSourceHandle.name;

      if (sourceNode.type == "genericNode") {
        // For components with group_outputs, each output has its own handle,
        // so we must NOT use selected_output (which would match the wrong output).
        // Only use selected_output for single-output dropdown components.
        const hasGroupOutputs = sourceNode.data.node!.outputs?.some(
          (o) => o.group_outputs,
        );
        const outputBySelectedOutput = hasGroupOutputs
          ? undefined
          : sourceNode.data.node!.outputs?.find(
              (output) => output.name === sourceNode.data.selected_output,
            );
        const outputByFallback = sourceNode.data.node!.outputs?.find(
          (output) =>
            (output.selected ||
              (sourceNode.data.node!.outputs?.filter(
                (output) => !output.group_outputs,
              )?.length ?? 0) <= 1) &&
            output.name === name,
        );
        const output = outputBySelectedOutput ?? outputByFallback;

        if (output) {
          const outputTypes =
            output!.types.length === 1 ? output!.types : [output!.selected!];

          const id: sourceHandleType = {
            id: sourceNode.data.id,
            name: output?.name ?? name,
            output_types: outputTypes,
            dataType: sourceNode.data.type,
          };

          // Skip edge cleanup for outputs with allows_loop=true
          const hasAllowsLoop = output?.allows_loop === true;
          // Backward compatibility: old flows may have Data/DataFrame types that need to match JSON/Table
          const expectedSourceHandle = scapedJSONStringfy(id);
          const sourceMatchResult = handlesMatch(
            expectedSourceHandle,
            sourceHandle,
          );
          if (!sourceMatchResult && !hasAllowsLoop) {
            newEdges = newEdges.filter((e) => e.id !== edge.id);
            brokenEdges.push(generateAlertObject(sourceNode, targetNode, edge));
          } else if (
            sourceMatchResult &&
            expectedSourceHandle !== sourceHandle
          ) {
            // Handles match via migration but IDs differ — update edge to use current types
            const edgeInNewEdges = newEdges.find((e) => e.id === edge.id);
            if (edgeInNewEdges) {
              edgeInNewEdges.sourceHandle = expectedSourceHandle;
              if (edgeInNewEdges.data) {
                edgeInNewEdges.data.sourceHandle =
                  scapeJSONParse(expectedSourceHandle);
              }
              // Update edge ID to reflect new handles
              edgeInNewEdges.id =
                "reactflow__edge-" +
                edgeInNewEdges.source +
                expectedSourceHandle +
                "-" +
                edgeInNewEdges.target +
                (edgeInNewEdges.targetHandle ?? "");
            }
          }
        } else {
          newEdges = newEdges.filter((e) => e.id !== edge.id);
          brokenEdges.push(generateAlertObject(sourceNode, targetNode, edge));
        }
      }
    }

    newEdges = filterHiddenFieldsEdges(edge, newEdges, targetNode);
  });

  return { edges: newEdges, brokenEdges };
}

export function clearHandlesFromAdvancedFields(
  componentId: string,
  data: APIClassType,
): void {
  if (!componentId || !data?.template) {
    return;
  }

  try {
    const flowStore = useFlowStore.getState();
    const { edges, deleteEdge } = flowStore;

    const connectedEdges = edges.filter((edge) => edge.target === componentId);

    if (connectedEdges.length === 0) {
      return;
    }

    const edgeIdsToDelete: string[] = [];

    for (const edge of connectedEdges) {
      const fieldName = edge.data?.targetHandle?.fieldName;

      if (fieldName && isAdvancedField(data, fieldName)) {
        edgeIdsToDelete.push(edge.id);
      }
    }

    edgeIdsToDelete.forEach(deleteEdge);
  } catch (error) {
    console.error("Error clearing handles from advanced fields:", error);
  }
}

const isAdvancedField = (data: APIClassType, fieldName: string): boolean => {
  const field = data.template[fieldName];
  return field && "advanced" in field && field.advanced === true;
};

export function filterHiddenFieldsEdges(
  edge: EdgeType,
  newEdges: EdgeType[],
  targetNode: AllNodeType,
) {
  if (targetNode) {
    const targetHandle = edge.data?.targetHandle;
    if (!targetHandle) return newEdges;

    const fieldName = targetHandle.fieldName;
    const nodeTemplates = targetNode.data.node!.template;

    // Only check the specific field the edge is connected to
    if (nodeTemplates[fieldName]?.show === false) {
      newEdges = newEdges.filter((e) => e.id !== edge.id);
    }
  }
  return newEdges;
}

export function unselectAllNodesEdges(nodes: Node[], edges: Edge[]) {
  nodes.forEach((node: Node) => {
    node.selected = false;
  });
  edges.forEach((edge: Edge) => {
    edge.selected = false;
  });
}

export function isValidConnection(
  connection: Connection,
  nodes?: AllNodeType[],
  edges?: EdgeType[],
): boolean {
  const { source, target, sourceHandle, targetHandle } = connection;
  if (source === target) {
    return false;
  }

  const nodesArray = nodes || useFlowStore.getState().nodes;
  const edgesArray = edges || useFlowStore.getState().edges;

  const targetHandleObject: targetHandleType = scapeJSONParse(targetHandle!);
  const sourceHandleObject: sourceHandleType = scapeJSONParse(sourceHandle!);

  // Helper to find the edge between two nodes
  function findEdgeBetween(srcId: string, tgtId: string) {
    return edgesArray.find((e) => e.source === srcId && e.target === tgtId);
  }

  // Modified hasCycle to return the path of edges forming the loop
  const findCyclePath = (
    node: AllNodeType,
    visited = new Set(),
    path: EdgeType[] = [],
  ): EdgeType[] | null => {
    if (visited.has(node.id)) return null;
    visited.add(node.id);
    for (const outgoer of getOutgoers(node, nodesArray, edgesArray)) {
      const edge = findEdgeBetween(node.id, outgoer.id);
      if (!edge) continue;
      if (outgoer.id === source) {
        // This edge would close the loop
        return [...path, edge];
      }
      const result = findCyclePath(outgoer, visited, [...path, edge]);
      if (result) return result;
    }
    return null;
  };

  // Check if target is an loop input (loop component)
  const isLoopInput = !!targetHandleObject.output_types;

  // For loop inputs, check if source types match any of the configured target output_types
  // (which already includes original type + loop_types from the output configuration)
  // Backward compatibility: old flows may have Data/DataFrame types that need to match JSON/Table
  const loopInputTypeCheck =
    isLoopInput &&
    (typesAreCompatible(
      sourceHandleObject.output_types,
      targetHandleObject.output_types || [],
    ) ||
      typeIsCompatibleWith(
        sourceHandleObject.dataType,
        targetHandleObject.output_types || [],
      ));
  if (
    typeIsCompatibleWith(
      sourceHandleObject.dataType,
      targetHandleObject.inputTypes || [],
    ) ||
    loopInputTypeCheck ||
    (targetHandleObject.output_types &&
      !loopInputTypeCheck &&
      (typeIsCompatibleWith(
        sourceHandleObject.dataType,
        targetHandleObject.output_types || [],
      ) ||
        typesAreCompatible(
          sourceHandleObject.output_types,
          targetHandleObject.output_types || [],
        ))) ||
    typesAreCompatible(
      sourceHandleObject.output_types,
      targetHandleObject.inputTypes || [],
    ) ||
    typeIsCompatibleWith(sourceHandleObject.dataType, [
      targetHandleObject.type,
    ]) ||
    sourceHandleObject.output_types.some((t) =>
      typeIsCompatibleWith(t, [targetHandleObject.type]),
    )
  ) {
    const targetNode = nodesArray.find((node) => node.id === target!);
    const targetNodeDataNode = targetNode?.data?.node;
    if (
      (!targetNodeDataNode &&
        !edgesArray.find((e) => e.targetHandle === targetHandle)) ||
      (targetNodeDataNode &&
        targetHandleObject.output_types &&
        !edgesArray.find((e) => e.targetHandle === targetHandle)) ||
      (targetNodeDataNode &&
        !targetHandleObject.output_types &&
        ((!targetNodeDataNode.template[targetHandleObject.fieldName].list &&
          !edgesArray.find((e) => e.targetHandle === targetHandle)) ||
          targetNodeDataNode.template[targetHandleObject.fieldName].list))
    ) {
      // If the current target handle is a loop component, allow connection immediately
      if (targetHandleObject.output_types) {
        return true;
      }
      // Check for loop and if any edge in the loop is a loop component
      let cyclePath: EdgeType[] | null = null;
      if (targetNode) {
        cyclePath = findCyclePath(targetNode);
      }
      if (cyclePath) {
        // Check if any edge in the cycle path is a loop component
        const hasLoopComponent = cyclePath.some((edge) => {
          try {
            const th = scapeJSONParse(edge.targetHandle!);
            return !!th.output_types;
          } catch {
            return false;
          }
        });
        if (!hasLoopComponent) {
          return false;
        }
      }
      return true;
    }
  }
  return false;
}

export function looksLikeVariableName(value: unknown): boolean {
  if (typeof value !== "string" || !value.trim()) return false;
  return /^[A-Z][A-Z0-9_]*$/i.test(value.trim());
}

export function removeApiKeys(flow: FlowType): FlowType {
  const cleanFlow = cloneDeep(flow);
  cleanFlow.data!.nodes.forEach((node) => {
    if (node.type !== "genericNode") return;
    const template = node.data.node!.template;
    for (const key in template) {
      const field = template[key];

      if (field.password) {
        // Preserve env/global variable names for api_key so imported flows
        // can still resolve credentials, but strip any raw secrets.
        if (
          key === "api_key" &&
          ((typeof field.value === "string" &&
            looksLikeVariableName(field.value)) ||
            field.load_from_db === true)
        ) {
          continue;
        }
        field.value = "";
        field.load_from_db = false;
      }

      // Handle MCP server configurations
      if (
        key === "mcp_server" &&
        field.value &&
        typeof field.value === "object"
      ) {
        cleanMcpConfig(field.value as MCPServerValue);
      }
    }
  });
  return cleanFlow;
}

export function updateTemplate(
  reference: APITemplateType,
  objectToUpdate: APITemplateType,
): APITemplateType {
  const clonedObject: APITemplateType = cloneDeep(reference);

  // Loop through each key in the reference object
  for (const key in clonedObject) {
    // If the key is not in the object to update, add it
    if (objectToUpdate[key] && objectToUpdate[key].value) {
      clonedObject[key].value = objectToUpdate[key].value;
    }
    if (
      objectToUpdate[key] &&
      objectToUpdate[key].advanced !== null &&
      objectToUpdate[key].advanced !== undefined
    ) {
      clonedObject[key].advanced = objectToUpdate[key].advanced;
    }
  }
  return clonedObject;
}

export const processFlows = (DbData: FlowType[], skipUpdate = true) => {
  const savedComponents: { [key: string]: APIClassType } = {};
  DbData.forEach(async (flow: FlowType) => {
    try {
      if (!flow.data) {
        return;
      }
      if (flow.data && flow.is_component) {
        (flow.data.nodes[0].data as NodeDataType).node!.display_name =
          flow.name;
        savedComponents[
          createRandomKey(
            (flow.data.nodes[0].data as NodeDataType).type,
            uid.randomUUID(5),
          )
        ] = cloneDeep((flow.data.nodes[0].data as NodeDataType).node!);
        return;
      }
      await processDataFromFlow(flow, !skipUpdate).catch((e) => {
        console.error(e);
      });
    } catch (e) {
      console.error(e);
    }
  });
  return { data: savedComponents, flows: DbData };
};

export const needsLayout = (nodes: AllNodeType[]) => {
  return nodes.some((node) => !node.position);
};

export async function processDataFromFlow(
  flow: FlowType,
  refreshIds = true,
): Promise<ReactFlowJsonObject<AllNodeType, EdgeType> | null> {
  const data = flow?.data ? flow.data : null;
  if (data) {
    processFlowEdges(flow);
    //add dropdown option to nodeOutputs
    processFlowNodes(flow);
    //add animation to text type edges
    updateEdges(data.edges);
    // updateNodes(data.nodes, data.edges);
    if (refreshIds) updateIds(data); // Assuming updateIds is defined elsewhere
    // add layout to nodes if not present
    if (needsLayout(data.nodes)) {
      const layoutedNodes = await getLayoutedNodes(data.nodes, data.edges);
      data.nodes = layoutedNodes;
    }
  }
  return data;
}

export function updateIds(
  { edges, nodes }: { edges: EdgeType[]; nodes: AllNodeType[] },
  selection?: OnSelectionChangeParams,
) {
  const idsMap = {};
  const selectionIds = selection?.nodes.map((n) => n.id);
  if (nodes) {
    nodes.forEach((node: AllNodeType) => {
      // Generate a unique node ID
      let newId = getNodeId(node.data.type);
      if (selection && !selectionIds?.includes(node.id)) {
        newId = node.id;
      }
      idsMap[node.id] = newId;
      node.id = newId;
      node.data.id = newId;
      // Add the new node to the list of nodes in state
    });
    selection?.nodes.forEach((sNode: Node) => {
      if (sNode.type === "genericNode") {
        const newId = idsMap[sNode.id];
        sNode.id = newId;
        sNode.data.id = newId;
      }
    });
  }
  const concatedEdges = [...edges, ...((selection?.edges as EdgeType[]) ?? [])];
  if (concatedEdges)
    concatedEdges.forEach((edge: EdgeType) => {
      edge.source = idsMap[edge.source];
      edge.target = idsMap[edge.target];

      const sourceHandleObject: sourceHandleType = scapeJSONParse(
        edge.sourceHandle!,
      );
      edge.sourceHandle = scapedJSONStringfy({
        ...sourceHandleObject,
        id: edge.source,
      });
      if (edge.data?.sourceHandle?.id) {
        edge.data.sourceHandle.id = edge.source;
      }
      const targetHandleObject: targetHandleType = scapeJSONParse(
        edge.targetHandle!,
      );
      edge.targetHandle = scapedJSONStringfy({
        ...targetHandleObject,
        id: edge.target,
      });
      if (edge.data?.targetHandle?.id) {
        edge.data.targetHandle.id = edge.target;
      }
      edge.id =
        "reactflow__edge-" +
        edge.source +
        edge.sourceHandle +
        "-" +
        edge.target +
        edge.targetHandle;
    });
  return idsMap;
}

export function validateNode(node: AllNodeType, edges: Edge[]): Array<string> {
  if (!node.data?.node?.template || !Object.keys(node.data.node.template)) {
    return [
      "We've noticed a potential issue with a Component in the flow. Please review it and, if necessary, submit a bug report with your exported flow file. Thank you for your help!",
    ];
  }

  const {
    type,
    node: { template },
  } = node.data;

  const displayName = node.data.node.display_name;

  return Object.keys(template).reduce((errors: Array<string>, t) => {
    if (
      node.type === "genericNode" &&
      template[t].required &&
      !(template[t].tool_mode && node?.data?.node?.tool_mode) &&
      template[t].show &&
      (template[t].value === undefined ||
        template[t].value === null ||
        template[t].value === "") &&
      !edges.some(
        (edge) =>
          (scapeJSONParse(edge.targetHandle!) as targetHandleType).fieldName ===
            t &&
          (scapeJSONParse(edge.targetHandle!) as targetHandleType).id ===
            node.id,
      )
    ) {
      errors.push(
        `${displayName || type} is missing ${getFieldTitle(template, t)}.`,
      );
    } else if (
      template[t].type === "dict" &&
      template[t].required &&
      template[t].show &&
      (template[t].value !== undefined ||
        template[t].value !== null ||
        template[t].value !== "")
    ) {
      if (hasDuplicateKeys(template[t].value))
        errors.push(
          `${displayName || type} (${getFieldTitle(
            template,
            t,
          )}) contains duplicate keys with the same values.`,
        );
      if (hasEmptyKey(template[t].value))
        errors.push(
          `${displayName || type} (${getFieldTitle(
            template,
            t,
          )}) field must not be empty.`,
        );
    }
    return errors;
  }, [] as string[]);
}

export function validateNodes(
  nodes: AllNodeType[],
  edges: EdgeType[],
): // this returns an array of tuples with the node id and the errors
Array<{ id: string; errors: Array<string> }> {
  if (nodes.length === 0) {
    return [
      {
        id: "",
        errors: [
          "No components found in the flow. Please add at least one component to the flow.",
        ],
      },
    ];
  }
  // validateNode(n, edges) returns an array of errors for the node
  const nodeMap = nodes.map((n) => ({
    id: n.id,
    errors: validateNode(n, edges),
  }));

  return nodeMap.filter((n) => n.errors?.length);
}

export function validateEdge(
  e: EdgeType,
  nodes: AllNodeType[],
  edges: EdgeType[],
): Array<string> {
  const targetHandleObject: targetHandleType = scapeJSONParse(e.targetHandle!);

  const loop = hasLoop(e, nodes, edges);
  if (targetHandleObject.output_types && !loop) {
    return [i18n.t("errors.incompleteLoop")];
  }
  return [];
}

function hasLoop(
  e: EdgeType,
  nodes: AllNodeType[],
  edges: EdgeType[],
): boolean {
  const source = e.source;
  const target = e.target;

  // Check if this connection would create a cycle
  const targetNode = nodes.find((n) => n.id === target);

  const hasCycle = (
    node,
    visited = new Set(),
    firstEdge: EdgeType | null = null,
  ): boolean => {
    if (visited.has(node.id)) return false;

    visited.add(node.id);

    for (const outgoer of getOutgoers(node, nodes, edges)) {
      const edge = edges.find(
        (e) => e.source === node.id && e.target === outgoer.id,
      );
      if (outgoer.id === source) {
        const sourceHandleObject = scapeJSONParse(
          firstEdge?.sourceHandle ?? edge?.sourceHandle ?? "",
        );
        const targetHandleObject: targetHandleType = scapeJSONParse(
          e.targetHandle!,
        );

        // For loop inputs, compare by name and id instead of full stringified comparison
        // This handles the case where loop inputs have additional loop_types in output_types
        if (
          targetHandleObject.output_types &&
          sourceHandleObject.name === targetHandleObject.name &&
          sourceHandleObject.id === targetHandleObject.id
        ) {
          return true;
        }

        // Fallback to original comparison for non-loop inputs
        const sourceHandleParsed = scapedJSONStringfy(sourceHandleObject);
        if (sourceHandleParsed === e.targetHandle) {
          return true;
        }
      }
      if (hasCycle(outgoer, visited, firstEdge || edge)) return true;
    }
    return false;
  };

  if (targetNode?.id === source) return false;
  return hasCycle(targetNode);
}

export function updateEdges(edges: EdgeType[]) {
  if (edges)
    edges.forEach((edge) => {
      const _targetHandleObject: targetHandleType = scapeJSONParse(
        edge.targetHandle!,
      );
      edge.className = "";
    });
}

export function addVersionToDuplicates(flow: FlowType, flows: FlowType[]) {
  const flowsWithoutUpdatedFlow = flows.filter((f) => f.id !== flow.id);

  const existingNames = flowsWithoutUpdatedFlow.map((item) => item.name);
  let newName = flow.name;
  let count = 1;

  while (existingNames.includes(newName)) {
    newName = `${flow.name} (${count})`;
    count++;
  }

  return newName;
}

export function addEscapedHandleIdsToEdges({
  edges,
}: addEscapedHandleIdsToEdgesType): EdgeType[] {
  const newEdges = cloneDeep(edges);
  newEdges.forEach((edge) => {
    let escapedSourceHandle = edge.sourceHandle;
    let escapedTargetHandle = edge.targetHandle;
    if (!escapedSourceHandle) {
      const sourceHandle = edge.data?.sourceHandle;
      if (sourceHandle) {
        escapedSourceHandle = getRightHandleId(sourceHandle);
        edge.sourceHandle = escapedSourceHandle;
      }
    }
    if (!escapedTargetHandle) {
      const targetHandle = edge.data?.targetHandle;
      if (targetHandle) {
        escapedTargetHandle = getLeftHandleId(targetHandle);
        edge.targetHandle = escapedTargetHandle;
      }
    }
  });
  return newEdges;
}
export function updateEdgesHandleIds({
  edges,
  nodes,
}: updateEdgesHandleIdsType): EdgeType[] {
  const newEdges = cloneDeep(edges);
  newEdges.forEach((edge) => {
    const sourceNodeId = edge.source;
    const targetNodeId = edge.target;
    const sourceNode = nodes.find((node) => node.id === sourceNodeId);
    const targetNode = nodes.find((node) => node.id === targetNodeId);
    const source = edge.sourceHandle;
    const target = edge.targetHandle;
    //right
    let newSource: sourceHandleType;
    //left
    let newTarget: targetHandleType;
    if (target && targetNode) {
      const field = target.split("|")[1];
      newTarget = {
        type: targetNode.data.node!.template[field].type,
        fieldName: field,
        id: targetNode.data.id,
        inputTypes: targetNode.data.node!.template[field].input_types,
      };
    }
    if (source && sourceNode && sourceNode.type === "genericNode") {
      const output_types =
        sourceNode.data.node!.output_types ??
        sourceNode.data.node!.base_classes!;
      newSource = {
        id: sourceNode.data.id,
        output_types,
        dataType: sourceNode.data.type,
        name: output_types.join(" | "),
      };
    }
    edge.sourceHandle = scapedJSONStringfy(newSource!);
    edge.targetHandle = scapedJSONStringfy(newTarget!);
    const newData = {
      sourceHandle: scapeJSONParse(edge.sourceHandle),
      targetHandle: scapeJSONParse(edge.targetHandle),
    };
    edge.data = newData;
  });
  return newEdges;
}

export function updateNewOutput({ nodes, edges }: updateEdgesHandleIdsType) {
  const newEdges = cloneDeep(edges);
  const newNodes = cloneDeep(nodes);
  newEdges.forEach((edge) => {
    if (edge.sourceHandle && edge.targetHandle) {
      const newSourceHandle: sourceHandleType = scapeJSONParse(
        edge.sourceHandle,
      );
      const newTargetHandle: targetHandleType = scapeJSONParse(
        edge.targetHandle,
      );
      const id = newSourceHandle.id;
      const sourceNodeIndex = newNodes.findIndex((node) => node.id === id);
      let sourceNode: AllNodeType | undefined;
      if (sourceNodeIndex !== -1) {
        sourceNode = newNodes[sourceNodeIndex];
      }
      if (sourceNode?.type === "genericNode") {
        let intersection;
        if (newSourceHandle.baseClasses) {
          if (!newSourceHandle.output_types) {
            if (sourceNode?.data.node!.output_types) {
              newSourceHandle.output_types =
                sourceNode?.data.node!.output_types;
            } else {
              newSourceHandle.output_types = newSourceHandle.baseClasses;
            }
          }
          delete newSourceHandle.baseClasses;
        }
        if (
          newTargetHandle.inputTypes &&
          newTargetHandle.inputTypes.length > 0
        ) {
          intersection = newSourceHandle.output_types.filter((type) =>
            newTargetHandle.inputTypes!.includes(type),
          );
        } else {
          intersection = newSourceHandle.output_types.filter(
            (type) => type === newTargetHandle.type,
          );
        }
        const selected = intersection[0];
        newSourceHandle.name = newSourceHandle.output_types.join(" | ");
        newSourceHandle.output_types = [selected];
        if (sourceNode) {
          if (!sourceNode.data.node?.outputs) {
            sourceNode.data.node!.outputs = [];
          }
          const types =
            sourceNode.data.node!.output_types ??
            sourceNode.data.node!.base_classes!;
          if (
            !sourceNode.data.node!.outputs.some(
              (output) => output.selected === selected,
            )
          ) {
            sourceNode.data.node!.outputs.push({
              types,
              selected: selected,
              name: types.join(" | "),
              display_name: types.join(" | "),
            });
          }
        }

        edge.sourceHandle = scapedJSONStringfy(newSourceHandle);
        if (edge.data) {
          edge.data.sourceHandle = newSourceHandle;
        }
      }
    }
  });
  return { nodes: newNodes, edges: newEdges };
}

export function handleKeyDown(
  e:
    | React.KeyboardEvent<HTMLInputElement>
    | React.KeyboardEvent<HTMLTextAreaElement>,
  inputValue: string | number | string[] | null | undefined,
  block: string,
) {
  //condition to fix bug control+backspace on Windows/Linux
  if (
    (typeof inputValue === "string" &&
      (e.metaKey === true || e.ctrlKey === true) &&
      e.key === "Backspace" &&
      (inputValue === block ||
        inputValue?.charAt(inputValue?.length - 1) === " " ||
        specialCharsRegex.test(inputValue?.charAt(inputValue?.length - 1)))) ||
    (IS_MAC && e.ctrlKey === true && e.key === "Backspace")
  ) {
    e.preventDefault();
    e.stopPropagation();
  }

  if (e.ctrlKey === true && e.key === "Backspace" && inputValue === block) {
    e.preventDefault();
    e.stopPropagation();
  }
}

export function handleOnlyIntegerInput(
  event: React.KeyboardEvent<HTMLInputElement>,
) {
  if (
    event.key === "." ||
    event.key === "-" ||
    event.key === "," ||
    event.key === "e" ||
    event.key === "E" ||
    event.key === "+"
  ) {
    event.preventDefault();
  }
}

export function getConnectedNodes(
  edge: Edge,
  nodes: Array<AllNodeType>,
): Array<AllNodeType> {
  const sourceId = edge.source;
  const targetId = edge.target;
  return nodes.filter((node) => node.id === targetId || node.id === sourceId);
}

export function convertObjToArray(singleObject: object | string, type: string) {
  if (type !== "dict") return [{ "": "" }];
  if (typeof singleObject === "string") {
    singleObject = JSON.parse(singleObject);
  }
  if (Array.isArray(singleObject)) return singleObject;

  const arrConverted: any[] = [];
  if (typeof singleObject === "object") {
    for (const key in singleObject) {
      if (Object.hasOwn(singleObject, key)) {
        const newObj = {};
        newObj[key] = singleObject[key];
        arrConverted.push(newObj);
      }
    }
  }
  return arrConverted;
}

export function convertArrayToObj(arrayOfObjects) {
  if (!Array.isArray(arrayOfObjects)) return arrayOfObjects;

  const objConverted = {};
  for (const obj of arrayOfObjects) {
    for (const key in obj) {
      if (Object.hasOwn(obj, key)) {
        objConverted[key] = obj[key];
      }
    }
  }
  return objConverted;
}

export function hasDuplicateKeys(array) {
  const keys = {};
  // Transforms an empty object into an object array without opening the 'editNode' modal to prevent the flow build from breaking.
  if (!Array.isArray(array)) array = [{ "": "" }];
  for (const obj of array) {
    for (const key in obj) {
      if (keys[key]) {
        return true;
      }
      keys[key] = true;
    }
  }
  return false;
}

export function hasEmptyKey(objArray) {
  // Transforms an empty object into an array without opening the 'editNode' modal to prevent the flow build from breaking.
  if (!Array.isArray(objArray)) objArray = [];
  for (const obj of objArray) {
    for (const key in obj) {
      if (Object.hasOwn(obj, key) && key === "") {
        return true; // Found an empty key
      }
    }
  }
  return false; // No empty keys found
}

export function convertValuesToNumbers(arr) {
  return arr.map((obj) => {
    const newObj = {};
    for (const key in obj) {
      if (Object.hasOwn(obj, key)) {
        let value = obj[key];
        if (/^\d+$/.test(value)) {
          value = value?.toString().trim();
        }
        newObj[key] =
          value === "" || isNaN(value) ? value.toString() : Number(value);
      }
    }
    return newObj;
  });
}

export function scapedJSONStringfy(json: object): string {
  return customStringify(json).replace(/"/g, "œ");
}
export function scapeJSONParse(json: string): any {
  const parsed = json.replace(/œ/g, '"');
  return JSON.parse(parsed);
}

/**
 * Map of old types to new types for migration compatibility.
 * Allows edges saved with old types (Data/DataFrame) to work with new types (JSON/Table).
 */
const TYPE_MIGRATIONS: Record<string, string> = {
  Data: "JSON",
  DataFrame: "Table",
};

/**
 * Check if a single type is compatible with any type in a list, considering migrations.
 * @param sourceType The type from the source output
 * @param targetTypes The list of acceptable types from the target input
 * @returns true if sourceType matches any targetType directly or via migration
 */
export function typeIsCompatibleWith(
  sourceType: string,
  targetTypes: string[],
): boolean {
  const migratedSource = TYPE_MIGRATIONS[sourceType] || sourceType;
  return targetTypes.some((targetType) => {
    const migratedTarget = TYPE_MIGRATIONS[targetType] || targetType;
    return (
      sourceType === targetType ||
      migratedSource === targetType ||
      sourceType === migratedTarget ||
      migratedSource === migratedTarget
    );
  });
}

/**
 * Check if any type from sourceTypes is compatible with any type in targetTypes.
 * @param sourceTypes The types from the source output
 * @param targetTypes The list of acceptable types from the target input
 * @returns true if any sourceType matches any targetType directly or via migration
 */
export function typesAreCompatible(
  sourceTypes: string[],
  targetTypes: string[],
): boolean {
  return sourceTypes.some((sourceType) =>
    typeIsCompatibleWith(sourceType, targetTypes),
  );
}

/**
 * Check if two handles match, considering type migrations (Data→JSON, DataFrame→Table).
 * This allows edges saved with old types to be compatible with new types.
 */
export function handlesMatch(
  expectedHandle: string,
  actualHandle: string,
): boolean {
  if (expectedHandle === actualHandle) {
    return true;
  }

  try {
    // Parse both handles to compare their components
    const expected = scapeJSONParse(expectedHandle);
    const actual = scapeJSONParse(actualHandle);

    // Check all properties except output_types/inputTypes
    if (expected.id !== actual.id || expected.name !== actual.name) {
      return false;
    }

    // For source handles (have dataType)
    if (
      expected.dataType !== undefined &&
      expected.dataType !== actual.dataType
    ) {
      return false;
    }

    // For target handles (have fieldName)
    if (
      expected.fieldName !== undefined &&
      expected.fieldName !== actual.fieldName
    ) {
      return false;
    }
    if (expected.type !== undefined && expected.type !== actual.type) {
      return false;
    }

    // Compare output_types with migration tolerance
    const expectedTypes = expected.output_types || expected.inputTypes || [];
    const actualTypes = actual.output_types || actual.inputTypes || [];

    if (expectedTypes.length !== actualTypes.length) {
      // Allow mismatch if one is migrated version of other
      return typesMatchWithMigration(expectedTypes, actualTypes);
    }

    // Check if types match exactly or via migration
    return typesMatchWithMigration(expectedTypes, actualTypes);
  } catch {
    // If parsing fails, fall back to direct comparison
    return false;
  }
}

/**
 * Check if two type arrays match, considering type migrations.
 */
function typesMatchWithMigration(
  expectedTypes: string[],
  actualTypes: string[],
): boolean {
  if (expectedTypes.length === 0 && actualTypes.length === 0) {
    return true;
  }

  // Check each expected type against actual types
  for (const expectedType of expectedTypes) {
    const migratedExpected = TYPE_MIGRATIONS[expectedType] || expectedType;
    const found = actualTypes.some((actualType) => {
      const migratedActual = TYPE_MIGRATIONS[actualType] || actualType;
      return (
        expectedType === actualType ||
        migratedExpected === actualType ||
        expectedType === migratedActual ||
        migratedExpected === migratedActual
      );
    });
    if (found) {
      return true;
    }
  }

  return false;
}

/**
 * Migrate TypeConverter nodes to sync outputs with output_type value.
 * This fixes the bug where loading a template shows "Message Output" even when output_type is "JSON".
 */
export function migrateTypeConverterNodes(
  nodes: AllNodeType[],
  edges?: EdgeType[],
): void {
  // Track which nodes were migrated and their new output types
  const migratedNodes = new Map<string, string[]>();

  nodes.forEach((node) => {
    // Only migrate TypeConverter components
    if (node.data?.node?.display_name !== "Type Convert") {
      return;
    }

    // Type guard: ensure this is a NodeDataType (not NoteDataType)
    if (!("node" in node.data)) {
      return;
    }

    const outputType = node.data.node?.template?.output_type?.value;
    const currentOutputs = node.data.node?.outputs || [];

    // Determine correct output based on output_type
    let correctOutput: OutputFieldType;
    let selectedOutput: string;
    let outputTypes: string[];

    if (outputType === "JSON" || outputType === "Data") {
      outputTypes = ["JSON"];
      correctOutput = {
        types: outputTypes,
        selected: "JSON",
        name: "data_output",
        display_name: "JSON Output",
        method: "convert_to_data",
      };
      selectedOutput = "data_output";
    } else if (outputType === "Table" || outputType === "DataFrame") {
      outputTypes = ["Table"];
      correctOutput = {
        types: outputTypes,
        selected: "Table",
        name: "dataframe_output",
        display_name: "Table Output",
        method: "convert_to_dataframe",
      };
      selectedOutput = "dataframe_output";
    } else {
      // Default to Message
      outputTypes = ["Message"];
      correctOutput = {
        types: outputTypes,
        selected: "Message",
        name: "message_output",
        display_name: "Message Output",
        method: "convert_to_message",
      };
      selectedOutput = "message_output";
    }

    // Check if migration is needed
    const needsMigration =
      currentOutputs.length !== 1 ||
      currentOutputs[0]?.name !== correctOutput.name;

    if (needsMigration) {
      // Update outputs
      node.data.node.outputs = [correctOutput];
      (node.data as any).selected_output = selectedOutput;

      // Track this migration for edge updates
      migratedNodes.set(node.id, outputTypes);
    }
  });

  // Update edges that connect to migrated nodes
  if (edges && migratedNodes.size > 0) {
    edges.forEach((edge) => {
      // Check if source node was migrated
      const sourceOutputTypes = migratedNodes.get(edge.source);
      if (sourceOutputTypes && edge.sourceHandle) {
        try {
          const sourceHandle = scapeJSONParse(edge.sourceHandle);
          // Update output_types in the handle
          sourceHandle.output_types = sourceOutputTypes;
          edge.sourceHandle = scapedJSONStringfy(sourceHandle);

          // Also update in edge.data if it exists
          if (edge.data?.sourceHandle) {
            edge.data.sourceHandle.output_types = sourceOutputTypes;
          }
        } catch {
          // Handle parse error silently
        }
      }
    });
  }
}

// this function receives an array of edges and return true if any of the handles are not a json string
export function checkOldEdgesHandles(edges: Edge[]): boolean {
  return edges.some(
    (edge) =>
      !edge.sourceHandle ||
      !edge.targetHandle ||
      !edge.sourceHandle.includes("{") ||
      !edge.targetHandle.includes("{"),
  );
}

export function checkEdgeWithoutEscapedHandleIds(edges: Edge[]): boolean {
  return edges.some(
    (edge) =>
      (!edge.sourceHandle || !edge.targetHandle) && edge.data?.sourceHandle,
  );
}

export function checkOldNodesOutput(nodes: AllNodeType[]): boolean {
  return nodes.some(
    (node) =>
      node.type === "genericNode" && node.data.node?.outputs === undefined,
  );
}

export function customStringify(obj: any): string {
  if (typeof obj === "undefined") {
    return "null";
  }

  if (obj === null || typeof obj !== "object") {
    if (obj instanceof Date) {
      return `"${obj.toISOString()}"`;
    }
    return JSON.stringify(obj);
  }

  if (Array.isArray(obj)) {
    const arrayItems = obj.map((item) => customStringify(item)).join(",");
    return `[${arrayItems}]`;
  }

  const keys = Object.keys(obj).sort();
  const keyValuePairs = keys.map(
    (key) => `"${key}":${customStringify(obj[key])}`,
  );
  return `{${keyValuePairs.join(",")}}`;
}

export function getMiddlePoint(nodes: Node[]) {
  let middlePointX = 0;
  let middlePointY = 0;

  nodes.forEach((node) => {
    middlePointX += node.position.x;
    middlePointY += node.position.y;
  });

  const totalNodes = nodes.length;
  const averageX = middlePointX / totalNodes;
  const averageY = middlePointY / totalNodes;

  return { x: averageX, y: averageY };
}

export function getNodeId(nodeType: string) {
  return nodeType + "-" + uid.randomUUID(5);
}

export function getHandleId(
  source: string,
  sourceHandle: string,
  target: string,
  targetHandle: string,
) {
  return (
    "reactflow__edge-" + source + sourceHandle + "-" + target + targetHandle
  );
}

export function generateFlow(
  selection: OnSelectionChangeParams,
  nodes: AllNodeType[],
  edges: EdgeType[],
  name: string,
): generateFlowType {
  const newFlowData = { nodes, edges, viewport: { zoom: 1, x: 0, y: 0 } };
  /*	remove edges that are not connected to selected nodes on both ends
   */
  newFlowData.edges = edges.filter(
    (edge) =>
      selection.nodes.some((node) => node.id === edge.target) &&
      selection.nodes.some((node) => node.id === edge.source),
  );
  newFlowData.nodes = selection.nodes as AllNodeType[];

  const newFlow: FlowType = {
    data: newFlowData,
    is_component: false,
    name: name,
    description: "",
    //generating local id instead of using the id from the server, can change in the future
    id: uid.randomUUID(5),
  };
  // filter edges that are not connected to selected nodes on both ends
  // using O(n²) aproach because the number of edges is small
  // in the future we can use a better aproach using a set
  return {
    newFlow,
    removedEdges: edges.filter(
      (edge) =>
        (selection.nodes.some((node) => node.id === edge.target) ||
          selection.nodes.some((node) => node.id === edge.source)) &&
        newFlowData.edges.every((e) => e.id !== edge.id),
    ),
  };
}

export function reconnectEdges(
  groupNode: AllNodeType,
  excludedEdges: EdgeType[],
) {
  if (groupNode.type !== "genericNode" || !groupNode.data.node!.flow) return [];
  let newEdges = cloneDeep(excludedEdges);
  const { nodes } = groupNode.data.node!.flow!.data!;
  const lastNode = findLastNode(groupNode.data.node!.flow!.data!);
  newEdges = newEdges.filter(
    (e) => !(nodes.some((n) => n.id === e.source) && e.source !== lastNode?.id),
  );
  newEdges.forEach((edge) => {
    const newSourceHandle: sourceHandleType = scapeJSONParse(
      edge.sourceHandle!,
    );
    const newTargetHandle: targetHandleType = scapeJSONParse(
      edge.targetHandle!,
    );
    if (lastNode && edge.source === lastNode.id) {
      edge.source = groupNode.id;
      newSourceHandle.id = groupNode.id;
      edge.sourceHandle = scapedJSONStringfy(newSourceHandle);
    }
    if (nodes.some((node) => node.id === edge.target)) {
      const targetNode = nodes.find((node) => node.id === edge.target)!;
      const proxy = { id: targetNode.id, field: newTargetHandle.fieldName };
      newTargetHandle.id = groupNode.id;
      newTargetHandle.proxy = proxy;
      edge.target = groupNode.id;
      newTargetHandle.fieldName =
        newTargetHandle.fieldName + "_" + targetNode.id;
      edge.targetHandle = scapedJSONStringfy(newTargetHandle);
    }
    if (newSourceHandle && newTargetHandle) {
      edge.data = {
        sourceHandle: newSourceHandle,
        targetHandle: newTargetHandle,
      };
    }
  });
  return newEdges;
}

export function filterFlow(
  selection: OnSelectionChangeParams,
  setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void,
  setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void,
) {
  setNodes((nodes) => nodes.filter((node) => !selection.nodes.includes(node)));
  setEdges((edges) => edges.filter((edge) => !selection.edges.includes(edge)));
}

export function findLastNode({ nodes, edges }: findLastNodeType) {
  /*
    this function receives a flow and return the last node
  */
  const lastNode = nodes.find((n) => !edges.some((e) => e.source === n.id));
  return lastNode;
}

export function updateFlowPosition(NewPosition: XYPosition, flow: FlowType) {
  const middlePoint = getMiddlePoint(flow.data!.nodes);
  const deltaPosition = {
    x: NewPosition.x - middlePoint.x,
    y: NewPosition.y - middlePoint.y,
  };
  return {
    ...flow,
    data: {
      ...flow.data!,
      nodes: flow.data!.nodes.map((node) => ({
        ...node,
        position: {
          x: node.position.x + deltaPosition.x,
          y: node.position.y + deltaPosition.y,
        },
      })),
    },
  };
}

export function concatFlows(
  flow: FlowType,
  setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void,
  setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void,
) {
  const { nodes, edges } = flow.data!;
  setNodes((old) => [...old, ...nodes]);
  setEdges((old) => [...old, ...edges]);
}

export function validateSelection(
  selection: OnSelectionChangeParams,
  edges: Edge[],
): Array<string> {
  const clonedSelection = cloneDeep(selection);
  const clonedEdges = cloneDeep(edges);
  //add edges to selection if selection mode selected only nodes
  if (clonedSelection.edges.length === 0) {
    clonedSelection.edges = clonedEdges;
  }

  // get only edges that are connected to the nodes in the selection
  // first creates a set of all the nodes ids
  const nodesSet = new Set(clonedSelection.nodes.map((n) => n.id));
  // then filter the edges that are connected to the nodes in the set
  const connectedEdges = clonedSelection.edges.filter(
    (e) => nodesSet.has(e.source) && nodesSet.has(e.target),
  );
  // add the edges to the selection
  clonedSelection.edges = connectedEdges;

  const errorsArray: Array<string> = [];
  // check if there is more than one node
  if (clonedSelection.nodes.length < 2) {
    errorsArray.push("Please select more than one component");
  }
  if (
    clonedSelection.nodes.some(
      (node) =>
        isInputNode(node.data as NodeDataType) ||
        isOutputNode(node.data as NodeDataType),
    )
  ) {
    errorsArray.push("Select non-input/output components only");
  }
  //check if there are two or more nodes with free outputs
  if (
    clonedSelection.nodes.filter(
      (n) => !clonedSelection.edges.some((e) => e.source === n.id),
    ).length > 1
  ) {
    errorsArray.push("Select only one component with free outputs");
  }

  // check if there is any node that does not have any connection
  if (
    clonedSelection.nodes.some(
      (node) =>
        !clonedSelection.edges.some((edge) => edge.target === node.id) &&
        !clonedSelection.edges.some((edge) => edge.source === node.id),
    )
  ) {
    errorsArray.push("Select only connected components");
  }
  return errorsArray;
}
function updateGroupNodeTemplate(template: APITemplateType) {
  /*this function receives a template, iterates for it's items
  updating the visibility of all basic types setting it to advanced true*/
  Object.keys(template).forEach((key) => {
    const type = template[key].type;
    const input_types = template[key].input_types;
    if (
      LANGFLOW_SUPPORTED_TYPES.has(type) &&
      !template[key].required &&
      !input_types
    ) {
      template[key].advanced = true;
    }
    //prevent code fields from showing on the group node
    if (type === "code" && key === "code") {
      template[key].show = false;
    }
  });
  return template;
}
export function mergeNodeTemplates({
  nodes,
  edges,
}: {
  nodes: AllNodeType[];
  edges: Edge[];
}): APITemplateType {
  /* this function receives a flow and iterate throw each node
    and merge the templates with only the visible fields
    if there are two keys with the same name in the flow, we will update the display name of each one
    to show from which node it came from
  */
  const template: APITemplateType = {};
  nodes.forEach((node) => {
    const nodeTemplate = cloneDeep(node.data.node!.template);
    Object.keys(nodeTemplate)
      .filter((field_name) => field_name.charAt(0) !== "_")
      .forEach((key) => {
        if (
          node.type === "genericNode" &&
          !isTargetHandleConnected(edges, key, nodeTemplate[key], node.id)
        ) {
          template[key + "_" + node.id] = nodeTemplate[key];
          template[key + "_" + node.id].proxy = { id: node.id, field: key };
          if (node.data.type === "GroupNode") {
            template[key + "_" + node.id].display_name =
              node.data.node!.flow!.name + " - " + nodeTemplate[key].name;
          } else {
            template[key + "_" + node.id].display_name =
              //data id already has the node name on it
              nodeTemplate[key].display_name
                ? nodeTemplate[key].display_name
                : nodeTemplate[key].name
                  ? toTitleCase(nodeTemplate[key].name)
                  : toTitleCase(key);
          }
        }
      });
  });
  return template;
}
export function isTargetHandleConnected(
  edges: Edge[],
  key: string,
  field: InputFieldType,
  nodeId: string,
) {
  /*
    this function receives a flow and a handleId and check if there is a connection with this handle
  */
  if (!field) return true;
  if (field.proxy) {
    if (
      edges.some(
        (e) =>
          e.targetHandle ===
          scapedJSONStringfy({
            type: field.type,
            fieldName: key,
            id: nodeId,
            proxy: { id: field.proxy!.id, field: field.proxy!.field },
            inputTypes: field.input_types,
          } as targetHandleType),
      )
    ) {
      return true;
    }
  } else {
    if (
      edges.some(
        (e) =>
          e.targetHandle ===
          scapedJSONStringfy({
            type: field.type,
            fieldName: key,
            id: nodeId,
            inputTypes: field.input_types,
          } as targetHandleType),
      )
    ) {
      return true;
    }
  }
  return false;
}

export function generateNodeTemplate(Flow: FlowType) {
  /*
    this function receives a flow and generate a template for the group node
  */
  const template = mergeNodeTemplates({
    nodes: Flow.data!.nodes,
    edges: Flow.data!.edges,
  });
  updateGroupNodeTemplate(template);
  return template;
}

export function generateNodeFromFlow(
  flow: FlowType,
  getNodeId: (type: string) => string,
): AllNodeType {
  const { nodes } = flow.data!;
  const _outputNode = cloneDeep(findLastNode(flow.data!));
  const position = getMiddlePoint(nodes);
  const data = cloneDeep(flow);
  const id = getNodeId("groupComponent");
  const newGroupNode: AllNodeType = {
    data: {
      id,
      type: "GroupNode",
      node: {
        display_name: "Group",
        documentation: "",
        description: "",
        template: generateNodeTemplate(data),
        flow: data,
        outputs: generateNodeOutputs(data),
      },
    },
    id,
    position,
    type: "genericNode",
  };
  return newGroupNode;
}

function generateNodeOutputs(flow: FlowType) {
  const { nodes, edges } = flow.data!;
  const outputs: Array<OutputFieldType> = [];
  nodes.forEach((node: AllNodeType) => {
    if (node.type === "genericNode" && node.data.node?.outputs) {
      const nodeOutputs = node.data.node.outputs;
      nodeOutputs.forEach((output) => {
        //filter outputs that are not connected
        if (
          !edges.some(
            (edge) =>
              edge.source === node.id &&
              (edge.data?.sourceHandle as sourceHandleType).name ===
                output.name,
          )
        ) {
          outputs.push(
            cloneDeep({
              ...output,
              proxy: {
                id: node.id,
                name: output.name,
                nodeDisplayName:
                  node.data.node!.display_name ?? node.data.node!.name,
              },
              name: node.id + "_" + output.name,
              display_name: output.display_name,
            }),
          );
        }
      });
    }
  });
  return outputs;
}

export function updateProxyIdsOnTemplate(
  template: APITemplateType,
  idsMap: { [key: string]: string },
) {
  Object.keys(template).forEach((key) => {
    if (template[key].proxy && idsMap[template[key].proxy!.id]) {
      template[key].proxy!.id = idsMap[template[key].proxy!.id];
    }
  });
}

export function updateProxyIdsOnOutputs(
  outputs: OutputFieldType[] | undefined,
  idsMap: { [key: string]: string },
) {
  if (!outputs) return;
  outputs.forEach((output) => {
    if (output.proxy && idsMap[output.proxy.id]) {
      output.proxy.id = idsMap[output.proxy.id];
    }
  });
}

export function updateEdgesIds(
  edges: EdgeType[],
  idsMap: { [key: string]: string },
) {
  edges.forEach((edge) => {
    const targetHandle: targetHandleType = edge.data!.targetHandle;
    if (targetHandle.proxy && idsMap[targetHandle.proxy!.id]) {
      targetHandle.proxy!.id = idsMap[targetHandle.proxy!.id];
    }
    edge.data!.targetHandle = targetHandle;
    edge.targetHandle = scapedJSONStringfy(targetHandle);
  });
}

export function processFlowEdges(flow: FlowType) {
  if (!flow.data || !flow.data.edges) return;
  if (checkEdgeWithoutEscapedHandleIds(flow.data.edges)) {
    const newEdges = addEscapedHandleIdsToEdges({ edges: flow.data.edges });
    flow.data.edges = newEdges;
  } else if (checkOldEdgesHandles(flow.data.edges)) {
    const newEdges = updateEdgesHandleIds(flow.data);
    flow.data.edges = newEdges;
  }
}

export function processFlowNodes(flow: FlowType) {
  if (!flow.data || !flow.data.nodes) return;
  if (checkOldNodesOutput(flow.data.nodes)) {
    const { nodes, edges } = updateNewOutput(flow.data);
    flow.data.nodes = nodes;
    flow.data.edges = edges;
  }
}

export function expandGroupNode(
  id: string,
  flow: FlowType,
  template: APITemplateType,
  setNodes: (
    update: AllNodeType[] | ((oldState: AllNodeType[]) => AllNodeType[]),
  ) => void,
  setEdges: (
    update: EdgeType[] | ((oldState: EdgeType[]) => EdgeType[]),
  ) => void,
  outputs?: OutputFieldType[],
) {
  const idsMap = updateIds(flow!.data!);
  updateProxyIdsOnTemplate(template, idsMap);
  const flowEdges = useFlowStore.getState().edges;
  updateEdgesIds(flowEdges, idsMap);
  const gNodes: AllNodeType[] = cloneDeep(flow?.data?.nodes!);
  const gEdges = cloneDeep(flow!.data!.edges);
  // //redirect edges to correct proxy node
  // let updatedEdges: Edge[] = [];
  // flowEdges.forEach((edge) => {
  //   let newEdge = cloneDeep(edge);
  //   if (newEdge.target === id) {
  //     const targetHandle: targetHandleType = newEdge.data.targetHandle;
  //     if (targetHandle.proxy) {
  //       let type = targetHandle.type;
  //       let field = targetHandle.proxy.field;
  //       let proxyId = targetHandle.proxy.id;
  //       let inputTypes = targetHandle.inputTypes;
  //       let node: NodeType = gNodes.find((n) => n.id === proxyId)!;
  //       if (node) {
  //         newEdge.target = proxyId;
  //         let newTargetHandle: targetHandleType = {
  //           fieldName: field,
  //           type,
  //           id: proxyId,
  //           inputTypes: inputTypes,
  //         };
  //         if (node.data.node?.flow) {
  //           newTargetHandle.proxy = {
  //             field: node.data.node.template[field].proxy?.field!,
  //             id: node.data.node.template[field].proxy?.id!,
  //           };
  //         }
  //         newEdge.data.targetHandle = newTargetHandle;
  //         newEdge.targetHandle = scapedJSONStringfy(newTargetHandle);
  //       }
  //     }
  //   }
  //   if (newEdge.source === id) {
  //     const lastNode = cloneDeep(findLastNode(flow!.data!));
  //     newEdge.source = lastNode!.id;
  //     let newSourceHandle: sourceHandleType = scapeJSONParse(
  //       newEdge.sourceHandle!,
  //     );
  //     newSourceHandle.id = lastNode!.id;
  //     newEdge.data.sourceHandle = newSourceHandle;
  //     newEdge.sourceHandle = scapedJSONStringfy(newSourceHandle);
  //   }
  //   if (edge.target === id || edge.source === id) {
  //     updatedEdges.push(newEdge);
  //   }
  // });
  //update template values
  Object.keys(template).forEach((key) => {
    if (template[key].proxy) {
      const { field, id } = template[key].proxy!;
      const nodeIndex = gNodes.findIndex((n) => n.id === id);
      if (nodeIndex !== -1) {
        let proxy: { id: string; field: string } | undefined;
        let display_name: string | undefined;
        const show = gNodes[nodeIndex].data.node!.template[field].show;
        const advanced = gNodes[nodeIndex].data.node!.template[field].advanced;
        if (gNodes[nodeIndex].data.node!.template[field].display_name) {
          display_name =
            gNodes[nodeIndex].data.node!.template[field].display_name;
        } else {
          display_name = gNodes[nodeIndex].data.node!.template[field].name;
        }
        if (gNodes[nodeIndex].data.node!.template[field].proxy) {
          proxy = gNodes[nodeIndex].data.node!.template[field].proxy;
        }
        gNodes[nodeIndex].data.node!.template[field] = template[key];
        gNodes[nodeIndex].data.node!.template[field].show = show;
        gNodes[nodeIndex].data.node!.template[field].advanced = advanced;
        gNodes[nodeIndex].data.node!.template[field].display_name =
          display_name;
        // keep the nodes selected after ungrouping
        // gNodes[nodeIndex].selected = false;
        if (proxy) {
          gNodes[nodeIndex].data.node!.template[field].proxy = proxy;
        } else {
          delete gNodes[nodeIndex].data.node!.template[field].proxy;
        }
      }
    }
  });
  outputs?.forEach((output) => {
    const nodeIndex = gNodes.findIndex((n) => n.id === output.proxy!.id);
    if (nodeIndex !== -1) {
      const node = gNodes[nodeIndex];
      if (node.type === "genericNode") {
        if (node.data.node?.outputs) {
          const nodeOutputIndex = node.data.node!.outputs!.findIndex(
            (o) => o.name === output.proxy?.name,
          );
          if (nodeOutputIndex !== -1 && output.selected) {
            node.data.node!.outputs![nodeOutputIndex].selected =
              output.selected;
          }
        }
      }
    }
  });
  const filteredNodes = [
    ...useFlowStore.getState().nodes.filter((n) => n.id !== id),
    ...gNodes,
  ];
  const filteredEdges = [
    ...flowEdges.filter((e) => e.target !== id && e.source !== id),
    ...gEdges,
  ];
  setNodes(filteredNodes);
  setEdges(filteredEdges);
}

export function getGroupStatus(
  flow: FlowType,
  ssData: { [key: string]: { valid: boolean; params: string } },
) {
  let status = { valid: true, params: i18n.t("flow.buildSuccess") };
  const { nodes } = flow.data!;
  const ids = nodes.map((n: AllNodeType) => n.data.id);
  ids.forEach((id) => {
    if (!ssData[id]) {
      status = ssData[id];
      return;
    }
    if (!ssData[id].valid) {
      status = { valid: false, params: ssData[id].params };
    }
  });
  return status;
}

export function createFlowComponent(
  nodeData: NodeDataType,
  version: string,
): FlowType {
  const flowNode: FlowType = {
    data: {
      edges: [],
      nodes: [
        {
          data: { ...nodeData, node: { ...nodeData.node, official: false } },
          id: nodeData.id,
          position: { x: 0, y: 0 },
          type: "genericNode",
        },
      ],
      viewport: { x: 1, y: 1, zoom: 1 },
    },
    description: nodeData.node?.description || "",
    name: nodeData.node?.display_name || nodeData.type || "",
    id: nodeData.id || "",
    is_component: true,
    last_tested_version: version,
  };
  return flowNode;
}

export async function downloadNode(NodeFLow: FlowType) {
  await customDownloadNodeJson(NodeFLow);
}

export function updateComponentNameAndType(
  data: any,
  component: NodeDataType,
) {}

export function removeFileNameFromComponents(flow: FlowType) {
  flow.data!.nodes.forEach((node: AllNodeType) => {
    if (node.type === "genericNode") {
      Object.keys(node.data.node!.template).forEach((field) => {
        if (node.data.node?.template[field].type === "file") {
          node.data.node!.template[field].value = "";
        }
      });
      if (node.data.node?.flow) {
        removeFileNameFromComponents(node.data.node.flow);
      }
    }
  });
}

export function removeGlobalVariableFromComponents(flow: FlowType) {
  flow.data!.nodes.forEach((node: AllNodeType) => {
    if (node.type === "genericNode") {
      Object.keys(node.data.node!.template).forEach((field) => {
        if (node.data?.node?.template[field]?.load_from_db) {
          node.data.node!.template[field].value = "";
          node.data.node!.template[field].load_from_db = false;
        }
      });
      if (node.data.node?.flow) {
        removeGlobalVariableFromComponents(node.data.node.flow);
      }
    }
  });
}

export function typesGenerator(data: APIObjectType) {
  return Object.keys(data)
    .reverse()
    .reduce((acc, curr) => {
      Object.keys(data[curr]).forEach((c: keyof APIKindType) => {
        acc[c] = curr;
        // Add the base classes to the accumulator as well.
        data[curr][c].base_classes?.forEach((b) => {
          acc[b] = curr;
        });
      });
      return acc;
    }, {});
}

const getTemplateAliases = (
  componentKey: string,
  component: Record<string, any>,
): string[] => {
  const aliases = [componentKey, component?.name, component?.display_name];
  const legacyAliases: Record<string, string[]> = {
    ParserComponent: ["parser"],
    "Prompt Template": ["Prompt"],
  };
  const componentType = component?.template?._type;

  aliases.push(...(legacyAliases[componentKey] ?? []));

  if (
    typeof componentType === "string" &&
    componentType.endsWith("Component")
  ) {
    aliases.push(componentType.replace(/Component$/, ""));
  }

  return Array.from(new Set(aliases.filter(Boolean)));
};

export function templatesGenerator(data: APIObjectType) {
  const templates = Object.entries(data).reduce<Record<string, APIClassType>>(
    (acc, [, kind]) => {
      Object.entries(kind).forEach(([componentKey, component]) => {
        //prevent wrong overwriting of the component template by a group of the same type
        if (!(component as APIClassType).flow) {
          acc[componentKey] = cloneDeep(component as APIClassType);
        }
      });
      return acc;
    },
    {},
  );

  for (const [componentKey, component] of Object.entries(templates)) {
    for (const alias of getTemplateAliases(componentKey, component)) {
      if (!(alias in templates)) {
        templates[alias] = component;
      }
    }
  }

  return templates;
}

/**
 * Determines if a field is a SecretStr field type
 */
function isSecretField(fieldData: any): boolean {
  // Check if field type is specifically SecretStr
  if (fieldData?.type === "SecretStr") {
    return true;
  }

  // Also check for fields that have both password=true and load_from_db=true
  // which are characteristics of SecretStrInput fields
  if (fieldData?.password === true && fieldData?.load_from_db === true) {
    return true;
  }

  return false;
}

/**
 * Extract only SecretStr type fields from components for global variables
 */
export function extractSecretFieldsFromComponents(data: APIObjectType) {
  const fields = new Set<string>();

  // Check if data exists
  if (!data) {
    console.warn(
      "[Types] Data is undefined in extractSecretFieldsFromComponents",
    );
    return fields;
  }

  Object.keys(data).forEach((key) => {
    // Check if data[key] exists
    if (!data[key]) {
      console.warn(
        `[Types] data["${key}"] is undefined in extractSecretFieldsFromComponents`,
      );
      return;
    }

    Object.keys(data[key]).forEach((kind) => {
      // Check if data[key][kind] exists
      if (!data[key][kind]) {
        console.warn(
          `[Types] data["${key}"]["${kind}"] is undefined in extractSecretFieldsFromComponents`,
        );
        return;
      }

      // Skip legacy components
      if (data[key][kind].legacy === true) {
        return;
      }

      // Check if template exists
      if (!data[key][kind].template) {
        console.warn(
          `[Types] data["${key}"]["${kind}"].template is undefined in extractSecretFieldsFromComponents`,
        );
        return;
      }

      Object.keys(data[key][kind].template).forEach((field) => {
        const fieldData = data[key][kind].template[field];
        if (
          fieldData?.display_name &&
          fieldData?.show &&
          isSecretField(fieldData)
        )
          fields.add(fieldData.display_name!);
      });
    });
  });

  return fields;
}

export function extractFieldsFromComponenents(data: APIObjectType) {
  const fields = new Set<string>();

  // Check if data exists
  if (!data) {
    console.warn("[Types] Data is undefined in extractFieldsFromComponenents");
    return fields;
  }

  Object.keys(data).forEach((key) => {
    // Check if data[key] exists
    if (!data[key]) {
      console.warn(
        `[Types] data["${key}"] is undefined in extractFieldsFromComponenents`,
      );
      return;
    }

    Object.keys(data[key]).forEach((kind) => {
      // Check if data[key][kind] exists
      if (!data[key][kind]) {
        console.warn(
          `[Types] data["${key}"]["${kind}"] is undefined in extractFieldsFromComponenents`,
        );
        return;
      }
      // Check if template exists
      if (!data[key][kind].template) {
        console.warn(
          `[Types] data["${key}"]["${kind}"].template is undefined in extractFieldsFromComponenents`,
        );
        return;
      }

      Object.keys(data[key][kind].template).forEach((field) => {
        if (
          data[key][kind].template[field]?.display_name &&
          data[key][kind].template[field]?.show
        )
          fields.add(data[key][kind].template[field].display_name!);
      });
    });
  });

  return fields;
}
/**
 * Recursively sorts all object keys and arrays in a JSON structure
 * @param obj - The object to sort keys and arrays for
 * @returns A new object with sorted keys and arrays
 */
function sortJsonStructure<T>(obj: T): T {
  // Handle null case
  if (obj === null) {
    return obj;
  }

  // Handle arrays - sort array elements if they are objects
  if (Array.isArray(obj)) {
    return obj.map((item) => sortJsonStructure(item)) as unknown as T;
  }

  // Only process actual objects
  if (typeof obj !== "object") {
    return obj;
  }

  // Create a new object with sorted keys
  return Object.keys(obj)
    .sort()
    .reduce((result, key) => {
      // Recursively sort nested objects and arrays
      result[key] = sortJsonStructure(obj[key]);
      return result;
    }, {} as any);
}

/**
 * Downloads the flow as a JSON file with sorted keys and arrays
 * @param flow - The flow to download
 * @param flowName - The name to use for the flow
 * @param flowDescription - Optional description for the flow
 */
export async function downloadFlow(
  flow: FlowType,
  flowName: string,
  flowDescription?: string,
): Promise<string | undefined | void> {
  try {
    const clonedFlow = cloneDeep(flow);

    removeFileNameFromComponents(clonedFlow);

    const flowData = {
      ...clonedFlow,
      name: flowName,
      description: flowDescription,
    };

    const sortedData = sortJsonStructure(flowData);
    const sortedJsonString = JSON.stringify(sortedData, null, 2);

    return await customDownloadFlow(flow, sortedJsonString, flowName);
  } catch (error) {
    console.error("Error downloading flow:", error);
    throw error;
  }
}

export function getRandomElement<T>(array: T[]): T {
  return array[Math.floor(Math.random() * array.length)];
}

export function getRandomDescription(): string {
  return getRandomElement(DESCRIPTIONS);
}

export const createNewFlow = (
  flowData: ReactFlowJsonObject<AllNodeType, EdgeType>,
  folderId: string,
  flow?: FlowType,
) => {
  return {
    description: flow?.description ?? getRandomDescription(),
    name: flow?.name ? flow.name : "New Flow",
    data: flowData,
    id: "",
    icon: flow?.icon ?? undefined,
    gradient: flow?.gradient ?? undefined,
    is_component: flow?.is_component ?? false,
    folder_id: folderId,
    endpoint_name: flow?.endpoint_name ?? undefined,
    tags: flow?.tags ?? [],
    locked: flow?.locked,
    mcp_enabled: true,
  };
};

export function isInputNode(nodeData: NodeDataType): boolean {
  return INPUT_TYPES.has(nodeData.type);
}

export function isOutputNode(nodeData: NodeDataType): boolean {
  return OUTPUT_TYPES.has(nodeData.type);
}

export function isInputType(type: string): boolean {
  return INPUT_TYPES.has(type);
}

export function isOutputType(type: string): boolean {
  return OUTPUT_TYPES.has(type);
}

export function updateGroupRecursion(
  groupNode: AllNodeType,
  edges: EdgeType[],
  unavailableFields:
    | {
        [name: string]: string;
      }
    | undefined,
  globalVariablesEntries: string[] | undefined,
) {
  if (groupNode.type === "genericNode") {
    updateGlobalVariables(
      groupNode.data.node,
      unavailableFields,
      globalVariablesEntries,
    );
    if (groupNode.data.node?.flow) {
      groupNode.data.node.flow.data!.nodes.forEach((node) => {
        if (node.type === "genericNode") {
          if (node.data.node?.flow) {
            updateGroupRecursion(
              node,
              node.data.node.flow.data!.edges,
              unavailableFields,
              globalVariablesEntries,
            );
          }
        }
      });
      const newFlow = groupNode.data.node!.flow;
      const idsMap = updateIds(newFlow.data!);
      updateProxyIdsOnTemplate(groupNode.data.node!.template, idsMap);
      updateProxyIdsOnOutputs(groupNode.data.node.outputs, idsMap);
      const flowEdges = edges;
      updateEdgesIds(flowEdges, idsMap);
    }
  }
}
export function updateGlobalVariables(
  node: APIClassType | undefined,
  unavailableFields:
    | {
        [name: string]: string;
      }
    | undefined,
  globalVariablesEntries: string[] | undefined,
) {
  if (node && node.template) {
    Object.keys(node.template).forEach((field) => {
      if (
        globalVariablesEntries &&
        node!.template[field].load_from_db &&
        !globalVariablesEntries.includes(node!.template[field].value)
      ) {
        node!.template[field].value = "";
        node!.template[field].load_from_db = false;
      }
      if (
        !node!.template[field].load_from_db &&
        node!.template[field].value === "" &&
        unavailableFields &&
        Object.keys(unavailableFields).includes(
          node!.template[field].display_name ?? "",
        )
      ) {
        node!.template[field].value =
          unavailableFields[node!.template[field].display_name ?? ""];
        node!.template[field].load_from_db = true;
      }
    });
  }
}

export function getGroupOutputNodeId(
  flow: FlowType,
  p_name: string,
  p_node_id: string,
) {
  const node: AllNodeType | undefined = flow.data?.nodes.find(
    (n) => n.id === p_node_id,
  );
  if (!node || node.type !== "genericNode") return;
  if (node.data.node?.flow) {
    const output = node.data.node.outputs?.find((o) => o.name === p_name);
    if (output && output.proxy) {
      return getGroupOutputNodeId(
        node.data.node.flow,
        output.proxy.name,
        output.proxy.id,
      );
    }
  }
  return { id: node.id, outputName: p_name };
}

export function checkOldComponents({ nodes }: { nodes: any[] }) {
  return nodes.some(
    (node) =>
      node.data.node?.template.code &&
      (node.data.node?.template.code.value as string).includes(
        "(CustomComponent):",
      ),
  );
}

export function someFlowTemplateFields(
  { nodes }: { nodes: AllNodeType[] },
  validateFn: (field: InputFieldType) => boolean,
): boolean {
  return nodes.some((node) => {
    return Object.keys(node.data.node?.template ?? {}).some((field) => {
      return validateFn((node.data.node?.template ?? {})[field]);
    });
  });
}

/**
 * Determines if the provided API template supports tool mode.
 *
 * A template is considered to support tool mode if either:
 * - It contains only the 'code' and '_type' fields (with both being truthy),
 *   indicating that no additional fields exist.
 * - At least one field in the template has a truthy 'tool_mode' property.
 *
 * @param template - The API template to evaluate.
 * @returns True if the template supports tool mode capability; otherwise, false.
 */
export function checkHasToolMode(template: APITemplateType): boolean {
  if (!template) return false;

  const templateKeys = Object.keys(template);

  // System/metadata fields that are not actual input fields
  const systemFields = ["code", "is_refresh", "tools_metadata"];

  // Count only fields that are actual inputs (not internal, not system fields)
  const inputFields = templateKeys.filter(
    (key) => !key.startsWith("_") && !systemFields.includes(key),
  );

  // Check if the template has no input fields
  // This means the component can support tool mode (it only has code and metadata)
  const hasNoInputFields =
    inputFields.length === 0 &&
    Boolean(template.code) &&
    Boolean(template._type);

  // Check if the template has at least one field with a truthy 'tool_mode' property
  const hasToolModeFields = Object.values(template).some((field) =>
    Boolean(field.tool_mode),
  );

  // Check if the component is already in tool mode
  // This occurs when the template has tools_metadata field
  const isInToolMode = Boolean(template.tools_metadata);

  return hasNoInputFields || hasToolModeFields || isInToolMode;
}

export function buildPositionDictionary(nodes: AllNodeType[]) {
  const positionDictionary = {};
  nodes.forEach((node) => {
    positionDictionary[node.position.x] = node.position.y;
  });
  return positionDictionary;
}

export function hasStreaming(nodes: AllNodeType[]) {
  return nodes.some((node) => node.data.node?.template?.stream?.value);
}

// Utility to get all connected nodes and edges from a given nodeId, in a given direction
export function getConnectedSubgraph(
  nodeId: string,
  nodes: AllNodeType[],
  edges: EdgeType[],
  direction: "upstream" | "downstream",
): { nodes: AllNodeType[]; edges: EdgeType[] } {
  const visited = new Set<string>();
  const resultNodes: AllNodeType[] = [];
  const resultEdges: EdgeType[] = [];

  function dfs(currentId: string) {
    if (visited.has(currentId)) return;
    visited.add(currentId);
    const node = nodes.find((n) => n.id === currentId);
    if (node) {
      resultNodes.push(node);
      if (direction === "upstream") {
        // Find all incoming edges
        const incomingEdges = edges.filter((e) => e.target === currentId);
        for (const edge of incomingEdges) {
          resultEdges.push(edge);
          dfs(edge.source);
        }
      } else {
        // downstream: Find all outgoing edges
        const outgoingEdges = edges.filter((e) => e.source === currentId);
        for (const edge of outgoingEdges) {
          resultEdges.push(edge);
          dfs(edge.target);
        }
      }
    }
  }
  dfs(nodeId);
  return {
    nodes: resultNodes,
    edges: resultEdges,
  };
}
