/**
 * This file implements a React Flow-based workflow editor component
 * that allows users to create, connect, and manage workflow nodes
 * in a visual graph interface.
 */

// External Dependencies
import shortid from 'short-unique-id';
import { diff } from 'deep-object-diff';
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { 
  ReactFlow, 
  Background, 
  BackgroundVariant, 
  useKeyPress,
  useNodesState,
  useReactFlow,
  useUpdateNodeInternals,
  ReactFlowProvider 
} from '@xyflow/react';

// Internal Components
import Confirm from '../Confirm';

// Signal Management and Utilities
import ApiSignal from '../../signals/Api';
import useFlowEdges from '../../hooks/useFlowEdges';
import useFlowNodes from '../../hooks/useFlowNodes';
import Signal, { useSignal } from '../../signal';
import NodesSignal, { ComponentsSignal } from '../../signals/Nodes';
import { fromXYFlow, toXYFlow, isValidConnection } from '../../utilities/workflow';

/**
 * Configuration for UUID generation
 * Used for creating unique identifiers for new nodes
 */
const UUID_CONFIG = {
  length     : 10,
  dictionary : 'hex'
};

// Initialize UUID generator
const uid = new shortid(UUID_CONFIG);

/**
 * Global signal states for managing output and selection
 * These are accessible throughout the application
 */
export const output = new Signal.State(null);
export const selected = new Signal.State(null);

// debouncer
let timeout;
const debounce = (fn, delay = 500) => {
  clearTimeout(timeout);
  timeout = setTimeout(fn, delay);
};

/**
 * Main Workflow Component
 * Provides the visual interface for creating and managing workflow nodes
 * 
 * @param {Object} props - Component props
 * @param {string} props.value - YAML string containing workflow data
 * @param {Function} props.onChange - Callback for workflow changes
 * @param {any} props.output - Output value to be managed by signal
 */
const WorkflowComponent = ({ value, onChange, output: outputProp }) => {
  // ReactFlow Hooks
  const updateNodeInternals = useUpdateNodeInternals();
  const { screenToFlowPosition } = useReactFlow();
  const deletePressed = useKeyPress('Delete');
  
  // Refs for managing flow state
  const flowRef = useRef(null);
  const reactFlowWrapper = useRef(null);
  const movingNodeId = useRef(null);
  const connectedNodeId = useRef(null);
  const connectingNodeId = useRef(null);

  // Component State
  const [ready, setReady] = useState(false);
  const [remove, setRemove] = useState(null);
  const [nodes, setNodes, onNodesChange] = useNodesState([]);

  // Signal State
  const [api] = useSignal(ApiSignal);
  const [sNodes] = useSignal(NodesSignal);
  const [sSelected] = useSignal(selected);
  const [,setOutput] = useSignal(output);
  const [sComponents] = useSignal(ComponentsSignal);

  // Custom hooks for managing flow state
  const { onNodeChange } = useFlowNodes(value, setNodes, updateNodeInternals);
  const edges = useFlowEdges(nodes, value, sNodes);

  /**
   * Handles node removal
   * Removes the selected node and updates the workflow
   */
  const handleNodeRemove = useCallback(() => {
    if (!remove?.id) return;
    const newNodes = nodes.filter((n) => n.id !== remove.id);
    onChange(newNodes.map(fromXYFlow));
    setRemove(null);
  }, [nodes, remove?.id, onChange]);

  /**
   * Creates a new node with given parameters
   * 
   * @param {Object} position - X,Y coordinates for the new node
   * @param {string} type - Node type (defaults to 'choose')
   * @param {Object} additionalData - Extra data to be included in the node
   * @returns {Object} New node configuration
   */
  const createNewNode = useCallback((position, type = 'choose', additionalData = {}) => {
    const id = uid.rnd();
    return {
      id,
      type,
      width: 280,
      position,
      origin: [0.0, 0.0],
      data: {
        ...additionalData,
        onChange: (key, value) => onNodeChange(id, key, value),
        onUpdate: () => updateNodeInternals(id),
      }
    };
  }, [onNodeChange, updateNodeInternals]);

  /**
   * Handles double-click events on the flow canvas
   * Creates a new node at the clicked position
   */
  const handleDoubleClick = useCallback((event) => {
    // Validate click target
    if (!event.target.classList.contains('react-flow__pane') || 
        event.target.classList.contains('react-flow__handle') ||
        event.target.classList.contains('react-flow__node')) return;

    event.preventDefault();
    event.stopPropagation();

    // Convert screen coordinates to flow coordinates
    const position = screenToFlowPosition({
      x: event.clientX,
      y: event.clientY,
    });

    // Create and add new node
    const newNode = createNewNode(position);
    const newNodes = [...nodes, newNode];
    
    onChange(newNodes.map(fromXYFlow));
  }, [nodes, screenToFlowPosition, createNewNode, onChange]);

  // handle paste
  const handlePaste = useCallback((event) => {
    // try/catch json
    try {
      // get pasted data
      const pastedData = event.clipboardData.getData('text');

      // check starts with {
      if (!pastedData.startsWith('{')) return;

      // parse json
      const parsedData = JSON.parse(pastedData);

      // Create and add new node
      const newNode = {
        ...parsedData,

        // update id and position
        id       : uid.rnd(),
        position : {
          x : parsedData.position.x + 10,
          y : parsedData.position.y + 10
        }
      };

      // new nodes
      const newNodes = [...nodes, toXYFlow(newNode, onNodeChange, updateNodeInternals)];
    
      onChange(newNodes.map(fromXYFlow));
    } catch (e) {
      // log error
      console.error(`error pasting: ${e}`);
    }
  }, [nodes, setNodes]);

  /**
   * Handles node connections
   * Updates node input connections when nodes are connected
   */
  const handleConnect = useCallback((params) => {
    connectedNodeId.current = params.target;
    if (!params.targetHandle) return;
    
    onNodeChange(
      params.target,
      `input.${params.targetHandle.split('-')[2]}`,
      params.sourceHandle.split('-').slice(1).join('-')
    );
  }, [onNodeChange]);

  /**
   * Handles the start of a connection
   * Tracks which node is being connected from
   */
  const handleConnectStart = useCallback((_, data) => {
    connectingNodeId.current = data.handleId.split('-').slice(1).join('-');
  }, []);

  /**
   * Handles the start of connection movement
   * Tracks which node's connection is being moved
   */
  const handleConnectMoveStart = useCallback((_, data) => {
    movingNodeId.current = data.id;
  }, []);

  /**
   * Handles the end of a connection attempt
   * Creates new node if dropped on canvas or updates existing connections
   */
  const handleConnectEnd = useCallback((event) => {
    // Handle existing connection updates
    if (connectedNodeId.current) {
      connectedNodeId.current = null;
      return;
    }

    // Handle moving existing connection
    if (movingNodeId.current) {
      const actualMovingNodeId = `${movingNodeId.current}`;
      
      // Remove the input connection
      onNodeChange(
        actualMovingNodeId.split('-')[0],
        `input.${actualMovingNodeId.split('-')[1]}`,
        null
      );

      movingNodeId.current = null;
      connectingNodeId.current = null;
      return;
    }

    // Handle new connection creation
    if (!connectingNodeId.current) return;
    if (!event.target.classList.contains('react-flow__pane')) return;
    if (event.target.classList.contains('react-flow__handle')) return;

    // Create new node at connection drop position
    const position = screenToFlowPosition({
      x: event.clientX,
      y: event.clientY,
    });

    const newNode = createNewNode(position);
    const newNodes = [...nodes, newNode];
    
    // on change
    onChange(newNodes.map(fromXYFlow));
  }, [nodes, screenToFlowPosition, createNewNode, onChange, onNodeChange]);

  // handle paste
  useEffect(() => {
    // Add the paste event listener when the component mounts
    document.addEventListener('paste', handlePaste);

    // Remove the paste event listener when the component unmounts
    return () => {
      document.removeEventListener('paste', handlePaste);
    };
  }, [handlePaste]);

  // Effect: Initialize ready state
  useEffect(() => {
    const timeout = setTimeout(() => setReady(true), 1000);
    return () => clearTimeout(timeout);
  }, []);

  // Effect: Handle delete key press
  useEffect(() => {
    if (sSelected && deletePressed) {
      const node = nodes.find((n) => n?.id === sSelected);
      setRemove(node);
    }
  }, [deletePressed, nodes, sSelected]);

  // Effect: Update output signal
  useEffect(() => {
    setOutput(outputProp);
  }, [outputProp, setOutput]);

  // Effect: Update nodes when YAML changes
  useEffect(() => {
    // check diff length
    if (!Object.keys(diff(value, (nodes || []).map(fromXYFlow))).length) return;
    
    // update nodes
    setNodes(value.map((item) => 
      toXYFlow(item, onNodeChange, updateNodeInternals)
    ));
  }, [value]);
  useEffect(() => {
    // log nodes changed
    debounce(() => {
      // on change
      onChange((nodes || []).map(fromXYFlow));
    }, 1000);
  }, [JSON.stringify((nodes || []).map(fromXYFlow))]);

  // Return null if component isn't ready or has no nodes
  if (!Object.keys(sComponents || {})?.length || (!value?.length && !ready)) {
    return null;
  }

  return (
    <div className="flex flex-1 min-w-0" ref={reactFlowWrapper}>
      <ReactFlow
        fitView
        snapToGrid
        nodes={nodes}
        edges={edges}
        nodeTypes={sComponents}
        onInit={(inst) => { 
          flowRef.current = inst; 
          inst.fitView({ includeHiddenNodes: true }); 
        }}
        onConnect={handleConnect}
        onNodesChange={onNodesChange}
        onConnectStart={handleConnectStart}
        onConnectEnd={handleConnectEnd}
        onReconnectStart={handleConnectMoveStart}
        isValidConnection={(connection) => isValidConnection(connection, value, sNodes)}
        zoomOnDoubleClick={false}
        onDoubleClickCapture={handleDoubleClick}
      >
        <Background color="#ccc" variant={BackgroundVariant.Dots} />
      </ReactFlow>

      <Confirm
        open={!!remove}
        title={api.translate('workflow:dialog.remove-node.title', 'Remove Node', remove)}
        submit={api.translate('workflow:button.remove', 'Remove')}
        onClose={() => setRemove(null)}
        subtitle={api.translate('workflow:dialog.remove-node.message', `Remove ${remove?.name}?`, remove)}
        onConfirm={handleNodeRemove}
        description={api.translate('workflow:dialog.remove-node.message', 
          `Are you sure you want to remove ${remove?.name}?`, remove)}
      />
    </div>
  );
};

/**
 * Default export wrapped in ReactFlowProvider
 * Ensures proper context is available to all child components
 */
export default (props) => (
  <ReactFlowProvider>
    <WorkflowComponent {...props} />
  </ReactFlowProvider>
);