/**
 * Workflow Diff Engine
 * Applies diff operations to n8n workflows
 */

import { v4 as uuidv4 } from 'uuid';
import {
  WorkflowDiffOperation,
  WorkflowDiffRequest,
  WorkflowDiffResult,
  WorkflowDiffValidationError,
  isNodeOperation,
  isConnectionOperation,
  isMetadataOperation,
  AddNodeOperation,
  RemoveNodeOperation,
  UpdateNodeOperation,
  MoveNodeOperation,
  EnableNodeOperation,
  DisableNodeOperation,
  AddConnectionOperation,
  RemoveConnectionOperation,
  RewireConnectionOperation,
  UpdateSettingsOperation,
  UpdateNameOperation,
  AddTagOperation,
  RemoveTagOperation,
  CleanStaleConnectionsOperation,
  ReplaceConnectionsOperation
} from '../types/workflow-diff';
import { Workflow, WorkflowNode, WorkflowConnection } from '../types/n8n-api';
import { Logger } from '../utils/logger';
import { validateWorkflowNode, validateWorkflowConnections } from './n8n-validation';

const logger = new Logger({ prefix: '[WorkflowDiffEngine]' });

export class WorkflowDiffEngine {
  /**
   * Apply diff operations to a workflow
   */
  async applyDiff(
    workflow: Workflow,
    request: WorkflowDiffRequest
  ): Promise<WorkflowDiffResult> {
    try {
      // Clone workflow to avoid modifying original
      const workflowCopy = JSON.parse(JSON.stringify(workflow));

      // Group operations by type for two-pass processing
      const nodeOperationTypes = ['addNode', 'removeNode', 'updateNode', 'moveNode', 'enableNode', 'disableNode'];
      const nodeOperations: Array<{ operation: WorkflowDiffOperation; index: number }> = [];
      const otherOperations: Array<{ operation: WorkflowDiffOperation; index: number }> = [];

      request.operations.forEach((operation, index) => {
        if (nodeOperationTypes.includes(operation.type)) {
          nodeOperations.push({ operation, index });
        } else {
          otherOperations.push({ operation, index });
        }
      });

      const allOperations = [...nodeOperations, ...otherOperations];
      const errors: WorkflowDiffValidationError[] = [];
      const appliedIndices: number[] = [];
      const failedIndices: number[] = [];

      // Process based on mode
      if (request.continueOnError) {
        // Best-effort mode: continue even if some operations fail
        for (const { operation, index } of allOperations) {
          const error = this.validateOperation(workflowCopy, operation);
          if (error) {
            errors.push({
              operation: index,
              message: error,
              details: operation
            });
            failedIndices.push(index);
            continue;
          }

          try {
            this.applyOperation(workflowCopy, operation);
            appliedIndices.push(index);
          } catch (error) {
            const errorMsg = `Failed to apply operation: ${error instanceof Error ? error.message : 'Unknown error'}`;
            errors.push({
              operation: index,
              message: errorMsg,
              details: operation
            });
            failedIndices.push(index);
          }
        }

        // If validateOnly flag is set, return success without applying
        if (request.validateOnly) {
          return {
            success: errors.length === 0,
            message: errors.length === 0
              ? 'Validation successful. All operations are valid.'
              : `Validation completed with ${errors.length} errors.`,
            errors: errors.length > 0 ? errors : undefined,
            applied: appliedIndices,
            failed: failedIndices
          };
        }

        const success = appliedIndices.length > 0;
        return {
          success,
          workflow: workflowCopy,
          operationsApplied: appliedIndices.length,
          message: `Applied ${appliedIndices.length} operations, ${failedIndices.length} failed (continueOnError mode)`,
          errors: errors.length > 0 ? errors : undefined,
          applied: appliedIndices,
          failed: failedIndices
        };
      } else {
        // Atomic mode: all operations must succeed
        // Pass 1: Validate and apply node operations first
        for (const { operation, index } of nodeOperations) {
          const error = this.validateOperation(workflowCopy, operation);
          if (error) {
            return {
              success: false,
              errors: [{
                operation: index,
                message: error,
                details: operation
              }]
            };
          }

          try {
            this.applyOperation(workflowCopy, operation);
          } catch (error) {
            return {
              success: false,
              errors: [{
                operation: index,
                message: `Failed to apply operation: ${error instanceof Error ? error.message : 'Unknown error'}`,
                details: operation
              }]
            };
          }
        }

        // Pass 2: Validate and apply other operations (connections, metadata)
        for (const { operation, index } of otherOperations) {
          const error = this.validateOperation(workflowCopy, operation);
          if (error) {
            return {
              success: false,
              errors: [{
                operation: index,
                message: error,
                details: operation
              }]
            };
          }

          try {
            this.applyOperation(workflowCopy, operation);
          } catch (error) {
            return {
              success: false,
              errors: [{
                operation: index,
                message: `Failed to apply operation: ${error instanceof Error ? error.message : 'Unknown error'}`,
                details: operation
              }]
            };
          }
        }

        // If validateOnly flag is set, return success without applying
        if (request.validateOnly) {
          return {
            success: true,
            message: 'Validation successful. Operations are valid but not applied.'
          };
        }

        const operationsApplied = request.operations.length;
        return {
          success: true,
          workflow: workflowCopy,
          operationsApplied,
          message: `Successfully applied ${operationsApplied} operations (${nodeOperations.length} node ops, ${otherOperations.length} other ops)`
        };
      }
    } catch (error) {
      logger.error('Failed to apply diff', error);
      return {
        success: false,
        errors: [{
          operation: -1,
          message: `Diff engine error: ${error instanceof Error ? error.message : 'Unknown error'}`
        }]
      };
    }
  }


  /**
   * Validate a single operation
   */
  private validateOperation(workflow: Workflow, operation: WorkflowDiffOperation): string | null {
    switch (operation.type) {
      case 'addNode':
        return this.validateAddNode(workflow, operation);
      case 'removeNode':
        return this.validateRemoveNode(workflow, operation);
      case 'updateNode':
        return this.validateUpdateNode(workflow, operation);
      case 'moveNode':
        return this.validateMoveNode(workflow, operation);
      case 'enableNode':
      case 'disableNode':
        return this.validateToggleNode(workflow, operation);
      case 'addConnection':
        return this.validateAddConnection(workflow, operation);
      case 'removeConnection':
        return this.validateRemoveConnection(workflow, operation);
      case 'rewireConnection':
        return this.validateRewireConnection(workflow, operation as RewireConnectionOperation);
      case 'updateSettings':
      case 'updateName':
      case 'addTag':
      case 'removeTag':
        return null; // These are always valid
      case 'cleanStaleConnections':
        return this.validateCleanStaleConnections(workflow, operation);
      case 'replaceConnections':
        return this.validateReplaceConnections(workflow, operation);
      default:
        return `Unknown operation type: ${(operation as any).type}`;
    }
  }

  /**
   * Apply a single operation to the workflow
   */
  private applyOperation(workflow: Workflow, operation: WorkflowDiffOperation): void {
    switch (operation.type) {
      case 'addNode':
        this.applyAddNode(workflow, operation);
        break;
      case 'removeNode':
        this.applyRemoveNode(workflow, operation);
        break;
      case 'updateNode':
        this.applyUpdateNode(workflow, operation);
        break;
      case 'moveNode':
        this.applyMoveNode(workflow, operation);
        break;
      case 'enableNode':
        this.applyEnableNode(workflow, operation);
        break;
      case 'disableNode':
        this.applyDisableNode(workflow, operation);
        break;
      case 'addConnection':
        this.applyAddConnection(workflow, operation);
        break;
      case 'removeConnection':
        this.applyRemoveConnection(workflow, operation);
        break;
      case 'rewireConnection':
        this.applyRewireConnection(workflow, operation as RewireConnectionOperation);
        break;
      case 'updateSettings':
        this.applyUpdateSettings(workflow, operation);
        break;
      case 'updateName':
        this.applyUpdateName(workflow, operation);
        break;
      case 'addTag':
        this.applyAddTag(workflow, operation);
        break;
      case 'removeTag':
        this.applyRemoveTag(workflow, operation);
        break;
      case 'cleanStaleConnections':
        this.applyCleanStaleConnections(workflow, operation);
        break;
      case 'replaceConnections':
        this.applyReplaceConnections(workflow, operation);
        break;
    }
  }

  // Node operation validators
  private validateAddNode(workflow: Workflow, operation: AddNodeOperation): string | null {
    const { node } = operation;

    // Check if node with same name already exists (use normalization to prevent collisions)
    const normalizedNewName = this.normalizeNodeName(node.name);
    const duplicate = workflow.nodes.find(n =>
      this.normalizeNodeName(n.name) === normalizedNewName
    );
    if (duplicate) {
      return `Node with name "${node.name}" already exists (normalized name matches existing node "${duplicate.name}")`;
    }
    
    // Validate node type format
    if (!node.type.includes('.')) {
      return `Invalid node type "${node.type}". Must include package prefix (e.g., "n8n-nodes-base.webhook")`;
    }
    
    if (node.type.startsWith('nodes-base.')) {
      return `Invalid node type "${node.type}". Use "n8n-nodes-base.${node.type.substring(11)}" instead`;
    }
    
    return null;
  }

  private validateRemoveNode(workflow: Workflow, operation: RemoveNodeOperation): string | null {
    const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
    if (!node) {
      return this.formatNodeNotFoundError(workflow, operation.nodeId || operation.nodeName || '', 'removeNode');
    }
    
    // Check if node has connections that would be broken
    const hasConnections = Object.values(workflow.connections).some(conn => {
      return Object.values(conn).some(outputs => 
        outputs.some(connections => 
          connections.some(c => c.node === node.name)
        )
      );
    });
    
    if (hasConnections || workflow.connections[node.name]) {
      // This is a warning, not an error - connections will be cleaned up
      logger.warn(`Removing node "${node.name}" will break existing connections`);
    }
    
    return null;
  }

  private validateUpdateNode(workflow: Workflow, operation: UpdateNodeOperation): string | null {
    const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
    if (!node) {
      return this.formatNodeNotFoundError(workflow, operation.nodeId || operation.nodeName || '', 'updateNode');
    }
    return null;
  }

  private validateMoveNode(workflow: Workflow, operation: MoveNodeOperation): string | null {
    const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
    if (!node) {
      return this.formatNodeNotFoundError(workflow, operation.nodeId || operation.nodeName || '', 'moveNode');
    }
    return null;
  }

  private validateToggleNode(workflow: Workflow, operation: EnableNodeOperation | DisableNodeOperation): string | null {
    const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
    if (!node) {
      const operationType = operation.type === 'enableNode' ? 'enableNode' : 'disableNode';
      return this.formatNodeNotFoundError(workflow, operation.nodeId || operation.nodeName || '', operationType);
    }
    return null;
  }

  // Connection operation validators
  private validateAddConnection(workflow: Workflow, operation: AddConnectionOperation): string | null {
    // Check for common parameter mistakes (Issue #249)
    const operationAny = operation as any;
    if (operationAny.sourceNodeId || operationAny.targetNodeId) {
      const wrongParams: string[] = [];
      if (operationAny.sourceNodeId) wrongParams.push('sourceNodeId');
      if (operationAny.targetNodeId) wrongParams.push('targetNodeId');

      return `Invalid parameter(s): ${wrongParams.join(', ')}. Use 'source' and 'target' instead. Example: {type: "addConnection", source: "Node Name", target: "Target Name"}`;
    }

    // Check for missing required parameters
    if (!operation.source) {
      return `Missing required parameter 'source'. The addConnection operation requires both 'source' and 'target' parameters. Check that you're using 'source' (not 'sourceNodeId').`;
    }
    if (!operation.target) {
      return `Missing required parameter 'target'. The addConnection operation requires both 'source' and 'target' parameters. Check that you're using 'target' (not 'targetNodeId').`;
    }

    const sourceNode = this.findNode(workflow, operation.source, operation.source);
    const targetNode = this.findNode(workflow, operation.target, operation.target);

    if (!sourceNode) {
      const availableNodes = workflow.nodes
        .map(n => `"${n.name}" (id: ${n.id.substring(0, 8)}...)`)
        .join(', ');
      return `Source node not found: "${operation.source}". Available nodes: ${availableNodes}. Tip: Use node ID for names with special characters (apostrophes, quotes).`;
    }
    if (!targetNode) {
      const availableNodes = workflow.nodes
        .map(n => `"${n.name}" (id: ${n.id.substring(0, 8)}...)`)
        .join(', ');
      return `Target node not found: "${operation.target}". Available nodes: ${availableNodes}. Tip: Use node ID for names with special characters (apostrophes, quotes).`;
    }

    // Check if connection already exists
    const sourceOutput = operation.sourceOutput || 'main';
    const existing = workflow.connections[sourceNode.name]?.[sourceOutput];
    if (existing) {
      const hasConnection = existing.some(connections =>
        connections.some(c => c.node === targetNode.name)
      );
      if (hasConnection) {
        return `Connection already exists from "${sourceNode.name}" to "${targetNode.name}"`;
      }
    }

    return null;
  }

  private validateRemoveConnection(workflow: Workflow, operation: RemoveConnectionOperation): string | null {
    // If ignoreErrors is true, don't validate - operation will silently succeed even if connection doesn't exist
    if (operation.ignoreErrors) {
      return null;
    }

    const sourceNode = this.findNode(workflow, operation.source, operation.source);
    const targetNode = this.findNode(workflow, operation.target, operation.target);

    if (!sourceNode) {
      const availableNodes = workflow.nodes
        .map(n => `"${n.name}" (id: ${n.id.substring(0, 8)}...)`)
        .join(', ');
      return `Source node not found: "${operation.source}". Available nodes: ${availableNodes}. Tip: Use node ID for names with special characters.`;
    }
    if (!targetNode) {
      const availableNodes = workflow.nodes
        .map(n => `"${n.name}" (id: ${n.id.substring(0, 8)}...)`)
        .join(', ');
      return `Target node not found: "${operation.target}". Available nodes: ${availableNodes}. Tip: Use node ID for names with special characters.`;
    }

    const sourceOutput = operation.sourceOutput || 'main';
    const connections = workflow.connections[sourceNode.name]?.[sourceOutput];
    if (!connections) {
      return `No connections found from "${sourceNode.name}"`;
    }

    const hasConnection = connections.some(conns =>
      conns.some(c => c.node === targetNode.name)
    );

    if (!hasConnection) {
      return `No connection exists from "${sourceNode.name}" to "${targetNode.name}"`;
    }

    return null;
  }

  private validateRewireConnection(workflow: Workflow, operation: RewireConnectionOperation): string | null {
    // Validate source node exists
    const sourceNode = this.findNode(workflow, operation.source, operation.source);
    if (!sourceNode) {
      const availableNodes = workflow.nodes
        .map(n => `"${n.name}" (id: ${n.id.substring(0, 8)}...)`)
        .join(', ');
      return `Source node not found: "${operation.source}". Available nodes: ${availableNodes}. Tip: Use node ID for names with special characters.`;
    }

    // Validate "from" node exists (current target)
    const fromNode = this.findNode(workflow, operation.from, operation.from);
    if (!fromNode) {
      const availableNodes = workflow.nodes
        .map(n => `"${n.name}" (id: ${n.id.substring(0, 8)}...)`)
        .join(', ');
      return `"From" node not found: "${operation.from}". Available nodes: ${availableNodes}. Tip: Use node ID for names with special characters.`;
    }

    // Validate "to" node exists (new target)
    const toNode = this.findNode(workflow, operation.to, operation.to);
    if (!toNode) {
      const availableNodes = workflow.nodes
        .map(n => `"${n.name}" (id: ${n.id.substring(0, 8)}...)`)
        .join(', ');
      return `"To" node not found: "${operation.to}". Available nodes: ${availableNodes}. Tip: Use node ID for names with special characters.`;
    }

    // Resolve smart parameters (branch, case) before validating connections
    const { sourceOutput, sourceIndex } = this.resolveSmartParameters(workflow, operation);

    // Validate that connection from source to "from" exists at the specific index
    const connections = workflow.connections[sourceNode.name]?.[sourceOutput];
    if (!connections) {
      return `No connections found from "${sourceNode.name}" on output "${sourceOutput}"`;
    }

    if (!connections[sourceIndex]) {
      return `No connections found from "${sourceNode.name}" on output "${sourceOutput}" at index ${sourceIndex}`;
    }

    const hasConnection = connections[sourceIndex].some(c => c.node === fromNode.name);

    if (!hasConnection) {
      return `No connection exists from "${sourceNode.name}" to "${fromNode.name}" on output "${sourceOutput}" at index ${sourceIndex}"`;
    }

    return null;
  }

  // Node operation appliers
  private applyAddNode(workflow: Workflow, operation: AddNodeOperation): void {
    const newNode: WorkflowNode = {
      id: operation.node.id || uuidv4(),
      name: operation.node.name,
      type: operation.node.type,
      typeVersion: operation.node.typeVersion || 1,
      position: operation.node.position,
      parameters: operation.node.parameters || {},
      credentials: operation.node.credentials,
      disabled: operation.node.disabled,
      notes: operation.node.notes,
      notesInFlow: operation.node.notesInFlow,
      continueOnFail: operation.node.continueOnFail,
      onError: operation.node.onError,
      retryOnFail: operation.node.retryOnFail,
      maxTries: operation.node.maxTries,
      waitBetweenTries: operation.node.waitBetweenTries,
      alwaysOutputData: operation.node.alwaysOutputData,
      executeOnce: operation.node.executeOnce
    };
    
    workflow.nodes.push(newNode);
  }

  private applyRemoveNode(workflow: Workflow, operation: RemoveNodeOperation): void {
    const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
    if (!node) return;
    
    // Remove node from array
    const index = workflow.nodes.findIndex(n => n.id === node.id);
    if (index !== -1) {
      workflow.nodes.splice(index, 1);
    }
    
    // Remove all connections from this node
    delete workflow.connections[node.name];
    
    // Remove all connections to this node
    Object.keys(workflow.connections).forEach(sourceName => {
      const sourceConnections = workflow.connections[sourceName];
      Object.keys(sourceConnections).forEach(outputName => {
        sourceConnections[outputName] = sourceConnections[outputName].map(connections =>
          connections.filter(conn => conn.node !== node.name)
        ).filter(connections => connections.length > 0);
        
        // Clean up empty arrays
        if (sourceConnections[outputName].length === 0) {
          delete sourceConnections[outputName];
        }
      });
      
      // Clean up empty connection objects
      if (Object.keys(sourceConnections).length === 0) {
        delete workflow.connections[sourceName];
      }
    });
  }

  private applyUpdateNode(workflow: Workflow, operation: UpdateNodeOperation): void {
    const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
    if (!node) return;
    
    // Apply updates using dot notation
    Object.entries(operation.updates).forEach(([path, value]) => {
      this.setNestedProperty(node, path, value);
    });
  }

  private applyMoveNode(workflow: Workflow, operation: MoveNodeOperation): void {
    const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
    if (!node) return;
    
    node.position = operation.position;
  }

  private applyEnableNode(workflow: Workflow, operation: EnableNodeOperation): void {
    const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
    if (!node) return;
    
    node.disabled = false;
  }

  private applyDisableNode(workflow: Workflow, operation: DisableNodeOperation): void {
    const node = this.findNode(workflow, operation.nodeId, operation.nodeName);
    if (!node) return;
    
    node.disabled = true;
  }

  /**
   * Resolve smart parameters (branch, case) to technical parameters
   * Phase 1 UX improvement: Semantic parameters for multi-output nodes
   */
  private resolveSmartParameters(
    workflow: Workflow,
    operation: AddConnectionOperation | RewireConnectionOperation
  ): { sourceOutput: string; sourceIndex: number } {
    const sourceNode = this.findNode(workflow, operation.source, operation.source);

    // Start with explicit values or defaults
    let sourceOutput = operation.sourceOutput ?? 'main';
    let sourceIndex = operation.sourceIndex ?? 0;

    // Smart parameter: branch (for IF nodes)
    // IF nodes use 'main' output with index 0 (true) or 1 (false)
    if (operation.branch !== undefined && operation.sourceIndex === undefined) {
      // Only apply if sourceIndex not explicitly set
      if (sourceNode?.type === 'n8n-nodes-base.if') {
        sourceIndex = operation.branch === 'true' ? 0 : 1;
        // sourceOutput remains 'main' (do not change it)
      }
    }

    // Smart parameter: case (for Switch nodes)
    if (operation.case !== undefined && operation.sourceIndex === undefined) {
      // Only apply if sourceIndex not explicitly set
      sourceIndex = operation.case;
    }

    return { sourceOutput, sourceIndex };
  }

  // Connection operation appliers
  private applyAddConnection(workflow: Workflow, operation: AddConnectionOperation): void {
    const sourceNode = this.findNode(workflow, operation.source, operation.source);
    const targetNode = this.findNode(workflow, operation.target, operation.target);
    if (!sourceNode || !targetNode) return;

    // Resolve smart parameters (branch, case) to technical parameters
    const { sourceOutput, sourceIndex } = this.resolveSmartParameters(workflow, operation);

    // Use nullish coalescing to properly handle explicit 0 values
    const targetInput = operation.targetInput ?? 'main';
    const targetIndex = operation.targetIndex ?? 0;

    // Initialize source node connections object
    if (!workflow.connections[sourceNode.name]) {
      workflow.connections[sourceNode.name] = {};
    }

    // Initialize output type array
    if (!workflow.connections[sourceNode.name][sourceOutput]) {
      workflow.connections[sourceNode.name][sourceOutput] = [];
    }

    // Get reference to output array for clarity
    const outputArray = workflow.connections[sourceNode.name][sourceOutput];

    // Ensure we have connection arrays up to and including the target sourceIndex
    while (outputArray.length <= sourceIndex) {
      outputArray.push([]);
    }

    // Defensive: Verify the slot is an array (should always be true after while loop)
    if (!Array.isArray(outputArray[sourceIndex])) {
      outputArray[sourceIndex] = [];
    }

    // Add connection to the correct sourceIndex
    outputArray[sourceIndex].push({
      node: targetNode.name,
      type: targetInput,
      index: targetIndex
    });
  }

  private applyRemoveConnection(workflow: Workflow, operation: RemoveConnectionOperation): void {
    const sourceNode = this.findNode(workflow, operation.source, operation.source);
    const targetNode = this.findNode(workflow, operation.target, operation.target);
    // If ignoreErrors is true, silently succeed even if nodes don't exist
    if (!sourceNode || !targetNode) {
      if (operation.ignoreErrors) {
        return; // Gracefully handle missing nodes
      }
      return; // Should never reach here if validation passed, but safety check
    }
    
    const sourceOutput = operation.sourceOutput || 'main';
    const connections = workflow.connections[sourceNode.name]?.[sourceOutput];
    if (!connections) return;
    
    // Remove connection from all indices
    workflow.connections[sourceNode.name][sourceOutput] = connections.map(conns =>
      conns.filter(conn => conn.node !== targetNode.name)
    );

    // Remove trailing empty arrays only (preserve intermediate empty arrays to maintain indices)
    const outputConnections = workflow.connections[sourceNode.name][sourceOutput];
    while (outputConnections.length > 0 && outputConnections[outputConnections.length - 1].length === 0) {
      outputConnections.pop();
    }

    if (outputConnections.length === 0) {
      delete workflow.connections[sourceNode.name][sourceOutput];
    }
    
    if (Object.keys(workflow.connections[sourceNode.name]).length === 0) {
      delete workflow.connections[sourceNode.name];
    }
  }

  /**
   * Rewire a connection from one target to another
   * This is a semantic wrapper around removeConnection + addConnection
   * that provides clear intent: "rewire connection from X to Y"
   *
   * @param workflow - Workflow to modify
   * @param operation - Rewire operation specifying source, from, and to
   */
  private applyRewireConnection(workflow: Workflow, operation: RewireConnectionOperation): void {
    // Resolve smart parameters (branch, case) to technical parameters
    const { sourceOutput, sourceIndex } = this.resolveSmartParameters(workflow, operation);

    // First, remove the old connection (source → from)
    this.applyRemoveConnection(workflow, {
      type: 'removeConnection',
      source: operation.source,
      target: operation.from,
      sourceOutput: sourceOutput,
      targetInput: operation.targetInput
    });

    // Then, add the new connection (source → to)
    this.applyAddConnection(workflow, {
      type: 'addConnection',
      source: operation.source,
      target: operation.to,
      sourceOutput: sourceOutput,
      targetInput: operation.targetInput,
      sourceIndex: sourceIndex,
      targetIndex: 0 // Default target index for new connection
    });
  }

  // Metadata operation appliers
  private applyUpdateSettings(workflow: Workflow, operation: UpdateSettingsOperation): void {
    if (!workflow.settings) {
      workflow.settings = {};
    }
    Object.assign(workflow.settings, operation.settings);
  }

  private applyUpdateName(workflow: Workflow, operation: UpdateNameOperation): void {
    workflow.name = operation.name;
  }

  private applyAddTag(workflow: Workflow, operation: AddTagOperation): void {
    if (!workflow.tags) {
      workflow.tags = [];
    }
    if (!workflow.tags.includes(operation.tag)) {
      workflow.tags.push(operation.tag);
    }
  }

  private applyRemoveTag(workflow: Workflow, operation: RemoveTagOperation): void {
    if (!workflow.tags) return;
    
    const index = workflow.tags.indexOf(operation.tag);
    if (index !== -1) {
      workflow.tags.splice(index, 1);
    }
  }

  // Connection cleanup operation validators
  private validateCleanStaleConnections(workflow: Workflow, operation: CleanStaleConnectionsOperation): string | null {
    // This operation is always valid - it just cleans up what it finds
    return null;
  }

  private validateReplaceConnections(workflow: Workflow, operation: ReplaceConnectionsOperation): string | null {
    // Validate that all referenced nodes exist
    const nodeNames = new Set(workflow.nodes.map(n => n.name));

    for (const [sourceName, outputs] of Object.entries(operation.connections)) {
      if (!nodeNames.has(sourceName)) {
        return `Source node not found in connections: ${sourceName}`;
      }

      // outputs is the value from Object.entries, need to iterate its keys
      for (const outputName of Object.keys(outputs)) {
        const connections = outputs[outputName];
        for (const conns of connections) {
          for (const conn of conns) {
            if (!nodeNames.has(conn.node)) {
              return `Target node not found in connections: ${conn.node}`;
            }
          }
        }
      }
    }

    return null;
  }

  // Connection cleanup operation appliers
  private applyCleanStaleConnections(workflow: Workflow, operation: CleanStaleConnectionsOperation): void {
    const nodeNames = new Set(workflow.nodes.map(n => n.name));
    const staleConnections: Array<{ from: string; to: string }> = [];

    // If dryRun, only identify stale connections without removing them
    if (operation.dryRun) {
      for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
        if (!nodeNames.has(sourceName)) {
          for (const [outputName, connections] of Object.entries(outputs)) {
            for (const conns of connections) {
              for (const conn of conns) {
                staleConnections.push({ from: sourceName, to: conn.node });
              }
            }
          }
        } else {
          for (const [outputName, connections] of Object.entries(outputs)) {
            for (const conns of connections) {
              for (const conn of conns) {
                if (!nodeNames.has(conn.node)) {
                  staleConnections.push({ from: sourceName, to: conn.node });
                }
              }
            }
          }
        }
      }
      logger.info(`[DryRun] Would remove ${staleConnections.length} stale connections:`, staleConnections);
      return;
    }

    // Actually remove stale connections
    for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
      // If source node doesn't exist, mark all connections as stale
      if (!nodeNames.has(sourceName)) {
        for (const [outputName, connections] of Object.entries(outputs)) {
          for (const conns of connections) {
            for (const conn of conns) {
              staleConnections.push({ from: sourceName, to: conn.node });
            }
          }
        }
        delete workflow.connections[sourceName];
        continue;
      }

      // Check each connection
      for (const [outputName, connections] of Object.entries(outputs)) {
        const filteredConnections = connections.map(conns =>
          conns.filter(conn => {
            if (!nodeNames.has(conn.node)) {
              staleConnections.push({ from: sourceName, to: conn.node });
              return false;
            }
            return true;
          })
        ).filter(conns => conns.length > 0);

        if (filteredConnections.length === 0) {
          delete outputs[outputName];
        } else {
          outputs[outputName] = filteredConnections;
        }
      }

      // Clean up empty output objects
      if (Object.keys(outputs).length === 0) {
        delete workflow.connections[sourceName];
      }
    }

    logger.info(`Removed ${staleConnections.length} stale connections`);
  }

  private applyReplaceConnections(workflow: Workflow, operation: ReplaceConnectionsOperation): void {
    workflow.connections = operation.connections;
  }

  // Helper methods

  /**
   * Normalize node names to handle special characters and escaping differences.
   * Fixes issue #270: apostrophes and other special characters in node names.
   *
   * ⚠️ WARNING: Normalization can cause collisions between names that differ only in:
   * - Leading/trailing whitespace
   * - Multiple consecutive spaces vs single spaces
   * - Escaped vs unescaped quotes/backslashes
   * - Different types of whitespace (tabs, newlines, spaces)
   *
   * Examples of names that normalize to the SAME value:
   * - "Node 'test'" === "Node  'test'" (multiple spaces)
   * - "Node 'test'" === "Node\t'test'" (tab vs space)
   * - "Node 'test'" === "Node \\'test\\'" (escaped quotes)
   * - "Path\\to\\file" === "Path\\\\to\\\\file" (escaped backslashes)
   *
   * Best Practice: For node names with special characters, prefer using node IDs
   * to avoid ambiguity. Use n8n_get_workflow_structure() to get node IDs.
   *
   * @param name - The node name to normalize
   * @returns Normalized node name for safe comparison
   */
  private normalizeNodeName(name: string): string {
    return name
      .trim()                    // Remove leading/trailing whitespace
      .replace(/\\\\/g, '\\')    // FIRST: Unescape backslashes: \\ -> \ (must be first to handle multiply-escaped chars)
      .replace(/\\'/g, "'")      // THEN: Unescape single quotes: \' -> '
      .replace(/\\"/g, '"')      // THEN: Unescape double quotes: \" -> "
      .replace(/\s+/g, ' ');     // FINALLY: Normalize all whitespace (spaces, tabs, newlines) to single space
  }

  /**
   * Find a node by ID or name in the workflow.
   * Uses string normalization to handle special characters (Issue #270).
   *
   * @param workflow - The workflow to search in
   * @param nodeId - Optional node ID to search for
   * @param nodeName - Optional node name to search for
   * @returns The found node or null
   */
  private findNode(workflow: Workflow, nodeId?: string, nodeName?: string): WorkflowNode | null {
    // Try to find by ID first (exact match, no normalization needed for UUIDs)
    if (nodeId) {
      const nodeById = workflow.nodes.find(n => n.id === nodeId);
      if (nodeById) return nodeById;
    }

    // Try to find by name with normalization (handles special characters)
    if (nodeName) {
      const normalizedSearch = this.normalizeNodeName(nodeName);
      const nodeByName = workflow.nodes.find(n =>
        this.normalizeNodeName(n.name) === normalizedSearch
      );
      if (nodeByName) return nodeByName;
    }

    // Fallback: If nodeId provided but not found, try treating it as a name
    // This allows operations to work with either IDs or names flexibly
    if (nodeId && !nodeName) {
      const normalizedSearch = this.normalizeNodeName(nodeId);
      const nodeByName = workflow.nodes.find(n =>
        this.normalizeNodeName(n.name) === normalizedSearch
      );
      if (nodeByName) return nodeByName;
    }

    return null;
  }

  /**
   * Format a consistent "node not found" error message with helpful context.
   * Shows available nodes with IDs and tips about using node IDs for special characters.
   *
   * @param workflow - The workflow being validated
   * @param nodeIdentifier - The node ID or name that wasn't found
   * @param operationType - The operation being performed (e.g., "removeNode", "updateNode")
   * @returns Formatted error message with available nodes and helpful tips
   */
  private formatNodeNotFoundError(
    workflow: Workflow,
    nodeIdentifier: string,
    operationType: string
  ): string {
    const availableNodes = workflow.nodes
      .map(n => `"${n.name}" (id: ${n.id.substring(0, 8)}...)`)
      .join(', ');
    return `Node not found for ${operationType}: "${nodeIdentifier}". Available nodes: ${availableNodes}. Tip: Use node ID for names with special characters (apostrophes, quotes).`;
  }

  private setNestedProperty(obj: any, path: string, value: any): void {
    const keys = path.split('.');
    let current = obj;
    
    for (let i = 0; i < keys.length - 1; i++) {
      const key = keys[i];
      if (!(key in current) || typeof current[key] !== 'object') {
        current[key] = {};
      }
      current = current[key];
    }
    
    current[keys[keys.length - 1]] = value;
  }
}