import { useMemo } from 'react';
import { Node, Edge } from '@xyflow/react';
import { Trigger } from '../types/trigger';
import { Action } from '../types/action';
import {
  AvailableContext,
  ContextVariable,
  ContextVariableGroup,
  validateContextType,
} from '../types/workflow-context';

// Re-export for consumers
export type { AvailableContext, ContextVariable, ContextVariableGroup };
import { WorkflowNodeData } from '../types/reactflow';
import { sanitizeNamespace } from '../utils/namespace';

interface UseAvailableContextParams {
  /** All nodes in the workflow */
  nodes: Node<WorkflowNodeData>[];
  /** All edges connecting nodes */
  edges: Edge[];
  /** ID of the current node being configured */
  currentNodeId: string;
  /** Available trigger definitions from API */
  triggers: Trigger[];
  /** Available action definitions from API */
  actions: Action[];
}

/**
 * Computes available context variables for a given node position.
 * Only includes outputs from nodes that precede the current node in the flow.
 *
 * @example
 * const availableContext = useAvailableContext({
 *   nodes,
 *   edges,
 *   currentNodeId: 'node-123',
 *   triggers,
 *   actions,
 * });
 * // Returns { groups: [...] } with variables from Trigger and preceding actions
 */
export function useAvailableContext({
  nodes,
  edges,
  currentNodeId,
  triggers,
  actions,
}: UseAvailableContextParams): AvailableContext {
  return useMemo(() => {
    const groups: ContextVariableGroup[] = [];

    // Find all preceding nodes using edge traversal
    const precedingNodeIds = getPrecedingNodeIds(edges, currentNodeId);

    // Process trigger node if it precedes current node
    const triggerNode = nodes.find((n) => n.type === 'trigger');
    if (triggerNode && precedingNodeIds.has(triggerNode.id)) {
      const triggerDef = triggers.find((t) => {
        // Match by action type stored in node data, or by label
        const nodeActionType = triggerNode.data.actionType;
        if (nodeActionType && t.type === nodeActionType) return true;
        // Fallback: match by name -> type conversion
        const nameAsType = triggerNode.data.name.toLowerCase().replace(/\s+/g, '_');
        return t.type === nameAsType;
      });

      if (triggerDef?.available_fields && triggerDef.available_fields.length > 0) {
        const variables: ContextVariable[] = triggerDef.available_fields.map((field) => ({
          path: `Trigger.${field.name}`,
          label: formatFieldLabel(field.name),
          source: triggerDef.label,
          namespace: 'Trigger',
          type: validateContextType(field.type),
          description: field.description,
          example: field.example,
        }));

        groups.push({
          namespace: 'Trigger',
          sourceName: triggerDef.label,
          sourceType: 'trigger',
          variables,
        });
      }
    }

    // Process action nodes that precede current node
    const actionNodes = nodes.filter((n) => n.type === 'action' && precedingNodeIds.has(n.id));

    // Sort action nodes by their position in the flow
    const sortedActionNodes = sortNodesByFlowOrder(actionNodes, edges, triggerNode?.id);

    sortedActionNodes.forEach((node, index) => {
      const actionType = node.data.actionType;
      const actionDef = actions.find((a) => a.type === actionType);

      if (actionDef?.output_fields && actionDef.output_fields.length > 0) {
        // Use user-defined label or fallback to action type
        const userLabel = node.data.label || actionType || `action_${index + 1}`;
        const namespace = sanitizeNamespace(userLabel);

        const variables: ContextVariable[] = actionDef.output_fields.map((field) => ({
          path: `${namespace}.${field.name}`,
          label: formatFieldLabel(field.name),
          source: node.data.label || actionDef.label,
          namespace,
          type: validateContextType(field.type),
          description: field.description,
          example: field.example,
        }));

        // Display name includes action number for clarity
        const displayName = node.data.label
          ? `${node.data.label} (${actionDef.label})`
          : `${actionDef.label} #${index + 1}`;

        groups.push({
          namespace,
          sourceName: displayName,
          sourceType: 'action',
          variables,
        });
      }
    });

    return { groups };
  }, [nodes, edges, currentNodeId, triggers, actions]);
}

/**
 * Finds all node IDs that precede the given node in the workflow.
 * Traverses edges backwards from the target node.
 */
function getPrecedingNodeIds(edges: Edge[], targetNodeId: string): Set<string> {
  const preceding = new Set<string>();
  const visited = new Set<string>();

  function traverse(nodeId: string) {
    if (visited.has(nodeId)) return;
    visited.add(nodeId);

    // Find all edges where this node is the target
    const incomingEdges = edges.filter((e) => e.target === nodeId);

    incomingEdges.forEach((edge) => {
      preceding.add(edge.source);
      traverse(edge.source);
    });
  }

  traverse(targetNodeId);
  return preceding;
}

/**
 * Sorts action nodes by their order in the flow.
 * Uses DFS from the start node to determine execution order.
 *
 * Nodes not reachable from the start node (e.g., disconnected subgraphs)
 * are appended at the end to ensure no nodes are silently dropped.
 */
function sortNodesByFlowOrder(
  actionNodes: Node<WorkflowNodeData>[],
  edges: Edge[],
  startNodeId?: string,
): Node<WorkflowNodeData>[] {
  if (!startNodeId) return actionNodes;

  const sorted: Node<WorkflowNodeData>[] = [];
  const nodeMap = new Map(actionNodes.map((n) => [n.id, n]));
  const visited = new Set<string>();

  function traverse(nodeId: string) {
    // Find all edges where this node is the source
    const outgoingEdges = edges.filter((e) => e.source === nodeId);
    outgoingEdges.forEach((edge) => {
      if (!visited.has(edge.target) && nodeMap.has(edge.target)) {
        visited.add(edge.target);
        sorted.push(nodeMap.get(edge.target)!);
        traverse(edge.target);
      }
    });
  }

  traverse(startNodeId);

  // Append any nodes not reachable from the start node
  // This handles disconnected subgraphs or malformed workflows
  for (const node of actionNodes) {
    if (!visited.has(node.id)) {
      sorted.push(node);
    }
  }

  return sorted;
}

/**
 * Formats a field name into a human-readable display label.
 * Converts snake_case to Title Case.
 */
function formatFieldLabel(fieldName: string): string {
  return fieldName
    .split('_')
    .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
    .join(' ');
}
