/*
 * Copyright OpenSearch Contributors
 * SPDX-License-Identifier: Apache-2.0
 */

import { useCallback, useMemo } from 'react';
import { getIncomers, getOutgoers, useReactFlow } from '@xyflow/react';
/**
 * Enum defining node/edge visibility states for filtering operations.
 * Used throughout the hook to control which nodes/edges are included in results.
 *
 * @enum {string}
 */
export let Visibility = /*#__PURE__*/function (Visibility) {
  Visibility["Visible"] = "visible";
  Visibility["Hidden"] = "hidden";
  Visibility["Any"] = "any";
  return Visibility;
}({});

/**
 * Interface defining all available node relationship analysis methods.
 * Provides comprehensive graph traversal capabilities with visibility filtering.
 */

/**
 * Custom hook providing comprehensive node relationship analysis and graph traversal utilities.
 *
 * This hook leverages React Flow's built-in utilities (getIncomers, getOutgoers) combined with
 * custom logic to provide efficient graph navigation capabilities. All methods support visibility
 * filtering to work with both visible and hidden nodes/edges.
 *
 * @returns {NodeRelationshipActions} Object containing all relationship analysis methods
 */
export const useNodeRelationships = () => {
  const {
    getEdges,
    getNode,
    getNodes
  } = useReactFlow();

  /**
   * Retrieves all incoming edges for a specified node with visibility filtering.
   * Incoming edges are those where the specified node is the target.
   */
  const getIncomingEdges = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    if (visibility === Visibility.Visible) {
      return getEdges().filter(edge => edge.target === nodeId && !edge.hidden);
    }
    if (visibility === Visibility.Hidden) {
      return getEdges().filter(edge => edge.target === nodeId && !!edge.hidden);
    }
    return getEdges().filter(edge => edge.target === nodeId);
  }, [getEdges]);

  /**
   * Counts incoming edges for efficient checking without creating arrays.
   */
  const getIncomingEdgesCount = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    return getIncomingEdges(nodeId, visibility).length;
  }, [getIncomingEdges]);

  /**
   * Boolean check for incoming edges existence.
   */
  const hasIncomingEdges = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    return !!getIncomingEdgesCount(nodeId, visibility);
  }, [getIncomingEdgesCount]);

  /**
   * Retrieves all outgoing edges for a specified node with visibility filtering.
   * Outgoing edges are those where the specified node is the source.
   */
  const getOutgoingEdges = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    if (visibility === Visibility.Visible) {
      return getEdges().filter(edge => edge.source === nodeId && !edge.hidden);
    }
    if (visibility === Visibility.Hidden) {
      return getEdges().filter(edge => edge.source === nodeId && !!edge.hidden);
    }
    return getEdges().filter(edge => edge.source === nodeId);
  }, [getEdges]);

  /**
   * Counts outgoing edges for efficient checking without creating arrays.
   */
  const getOutgoingEdgesCount = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    return getOutgoingEdges(nodeId, visibility).length;
  }, [getOutgoingEdges]);

  /**
   * Boolean check for outgoing edges existence.
   */
  const hasOutgoingEdges = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    return !!getOutgoingEdgesCount(nodeId, visibility);
  }, [getOutgoingEdgesCount]);

  /**
   * Retrieves all parent nodes using React Flow's getIncomers utility.
   * Parents are nodes that have edges pointing to the specified node.
   */
  const getParentNodes = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    const node = getNode(nodeId);
    if (!node) return [];
    const nodes = getIncomers(node, getNodes(), getEdges());
    if (visibility === Visibility.Visible) {
      return nodes.filter(n => !n.hidden);
    }
    if (visibility === Visibility.Hidden) {
      return nodes.filter(n => !!n.hidden);
    }
    return nodes;
  }, [getNode, getNodes, getEdges]);

  /**
   * Counts parent nodes for efficient checking without creating arrays.
   */
  const getParentNodesCount = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    return getParentNodes(nodeId, visibility).length;
  }, [getParentNodes]);

  /**
   * Boolean check for parent nodes existence.
   */
  const hasParents = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    return !!getParentNodesCount(nodeId, visibility);
  }, [getParentNodesCount]);

  /**
   * Retrieves all children nodes using React Flow's getOutgoers utility.
   * Children are nodes that the specified node has edges pointing to.
   */
  const getChildrenNodes = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    const node = getNode(nodeId);
    if (!node) return [];
    const nodes = getOutgoers(node, getNodes(), getEdges());
    if (visibility === Visibility.Visible) {
      return nodes.filter(n => !n.hidden);
    }
    if (visibility === Visibility.Hidden) {
      return nodes.filter(n => !!n.hidden);
    }
    return nodes;
  }, [getNode, getNodes, getEdges]);

  /**
   * Counts children nodes for efficient checking without creating arrays.
   */
  const getChildrenNodesCount = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    return getChildrenNodes(nodeId, visibility).length;
  }, [getChildrenNodes]);

  /**
   * Boolean check for children nodes existence.
   */
  const hasChildren = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    return !!getChildrenNodesCount(nodeId, visibility);
  }, [getChildrenNodesCount]);

  /**
   * Retrieves all sibling nodes by finding children of all parents.
   * Uses Map for deduplication to handle nodes with multiple parents.
   * Note: The result includes the original node itself.
   */
  const getSiblingNodes = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    const parents = getParentNodes(nodeId, visibility);
    const siblings = parents.map(parent => getChildrenNodes(parent.id, visibility)).flat();
    const map = new Map();
    siblings.forEach(sibling => {
      if (!map.has(sibling.id)) {
        map.set(sibling.id, sibling);
      }
    });
    return Array.from(map.values());
  }, [getParentNodes, getChildrenNodes]);

  /**
   * Counts sibling nodes for efficient checking without creating arrays.
   */
  const getSiblingNodesCount = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    return getSiblingNodes(nodeId, visibility).length;
  }, [getSiblingNodes]);

  /**
   * Boolean check for sibling nodes existence.
   */
  const hasSiblings = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    return !!getSiblingNodesCount(nodeId, visibility);
  }, [getSiblingNodesCount]);

  /**
   * Determines if a node is an aggregate node by checking for aggregatedNodeId.
   * Aggregate nodes are created during grouping operations and contain metadata
   * about the original nodes they represent.
   *
   * Note: There's a bug in the Hidden visibility check - it should check for
   * node.hidden === true, not false.
   */
  const isAggregateNode = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    const node = getNode(nodeId);
    if (!node) return false;
    const hasAggregateId = !!(node !== null && node !== void 0 && node.data.aggregatedNodeId);
    if (visibility === Visibility.Visible) {
      return hasAggregateId && !node.hidden;
    }
    if (visibility === Visibility.Hidden) {
      return hasAggregateId && !!node.hidden;
    }
    return hasAggregateId;
  }, [getNode]);

  /**
   * Retrieves all nodes that share the same aggregatedNodeId.
   * Used for finding nodes that were grouped together during aggregation operations.
   * Excludes the original node from the results.
   */
  const getAggregateSiblings = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    const node = getNode(nodeId);
    if (!(node !== null && node !== void 0 && node.data.aggregatedNodeId)) return [];
    const nodes = getNodes().filter(n => n.data.aggregatedNodeId === (node === null || node === void 0 ? void 0 : node.data.aggregatedNodeId) && n.id !== node.id);
    if (visibility === Visibility.Visible) {
      return nodes.filter(n => !n.hidden);
    }
    if (visibility === Visibility.Hidden) {
      return nodes.filter(n => !!n.hidden);
    }
    return nodes;
  }, [getNode, getNodes]);

  /**
   * Counts aggregate sibling nodes for efficient checking.
   */
  const getAggregateSiblingsCount = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    return getAggregateSiblings(nodeId, visibility).length;
  }, [getAggregateSiblings]);

  /**
   * Boolean check for aggregate sibling nodes existence.
   */
  const hasAggregateSiblings = useCallback(function (nodeId) {
    let visibility = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Visibility.Any;
    return !!getAggregateSiblings(nodeId, visibility).length;
  }, [getAggregateSiblings]);
  return useMemo(() => ({
    getAggregateSiblings,
    getAggregateSiblingsCount,
    getChildrenNodes,
    getChildrenNodesCount,
    getIncomingEdges,
    getIncomingEdgesCount,
    getOutgoingEdges,
    getOutgoingEdgesCount,
    getParentNodes,
    getParentNodesCount,
    getSiblingNodes,
    getSiblingNodesCount,
    hasAggregateSiblings,
    hasChildren,
    hasIncomingEdges,
    hasOutgoingEdges,
    hasParents,
    hasSiblings,
    isAggregateNode
  }), [getAggregateSiblings, getAggregateSiblingsCount, getChildrenNodes, getChildrenNodesCount, getIncomingEdges, getIncomingEdgesCount, getOutgoingEdges, getOutgoingEdgesCount, getParentNodes, getParentNodesCount, getSiblingNodes, getSiblingNodesCount, hasAggregateSiblings, hasChildren, hasIncomingEdges, hasOutgoingEdges, hasParents, hasSiblings, isAggregateNode]);
};