/**
 * Expression Validator for n8n expressions
 * Validates expression syntax, variable references, and context availability
 */

interface ExpressionValidationResult {
  valid: boolean;
  errors: string[];
  warnings: string[];
  usedVariables: Set<string>;
  usedNodes: Set<string>;
}

interface ExpressionContext {
  availableNodes: string[];
  currentNodeName?: string;
  isInLoop?: boolean;
  hasInputData?: boolean;
}

export class ExpressionValidator {
  // Common n8n expression patterns
  private static readonly EXPRESSION_PATTERN = /\{\{([\s\S]+?)\}\}/g;
  private static readonly VARIABLE_PATTERNS = {
    json: /\$json(\.[a-zA-Z_][\w]*|\["[^"]+"\]|\['[^']+'\]|\[\d+\])*/g,
    node: /\$node\["([^"]+)"\]\.json/g,
    input: /\$input\.item(\.[a-zA-Z_][\w]*|\["[^"]+"\]|\['[^']+'\]|\[\d+\])*/g,
    items: /\$items\("([^"]+)"(?:,\s*(-?\d+))?\)/g,
    parameter: /\$parameter\["([^"]+)"\]/g,
    env: /\$env\.([a-zA-Z_][\w]*)/g,
    workflow: /\$workflow\.(id|name|active)/g,
    execution: /\$execution\.(id|mode|resumeUrl)/g,
    prevNode: /\$prevNode\.(name|outputIndex|runIndex)/g,
    itemIndex: /\$itemIndex/g,
    runIndex: /\$runIndex/g,
    now: /\$now/g,
    today: /\$today/g,
  };

  /**
   * Validate a single expression
   */
  static validateExpression(
    expression: string,
    context: ExpressionContext
  ): ExpressionValidationResult {
    const result: ExpressionValidationResult = {
      valid: true,
      errors: [],
      warnings: [],
      usedVariables: new Set(),
      usedNodes: new Set(),
    };

    // Handle null/undefined expression
    if (!expression) {
      return result;
    }

    // Handle null/undefined context
    if (!context) {
      result.valid = false;
      result.errors.push('Validation context is required');
      return result;
    }

    // Check for basic syntax errors
    const syntaxErrors = this.checkSyntaxErrors(expression);
    result.errors.push(...syntaxErrors);

    // Extract all expressions
    const expressions = this.extractExpressions(expression);
    
    for (const expr of expressions) {
      // Validate each expression
      this.validateSingleExpression(expr, context, result);
    }

    // Check for undefined node references
    this.checkNodeReferences(result, context);

    result.valid = result.errors.length === 0;
    return result;
  }

  /**
   * Check for basic syntax errors
   */
  private static checkSyntaxErrors(expression: string): string[] {
    const errors: string[] = [];

    // Check for unmatched brackets
    const openBrackets = (expression.match(/\{\{/g) || []).length;
    const closeBrackets = (expression.match(/\}\}/g) || []).length;
    
    if (openBrackets !== closeBrackets) {
      errors.push('Unmatched expression brackets {{ }}');
    }

    // Check for nested expressions (not supported in n8n)
    if (expression.includes('{{') && expression.includes('{{', expression.indexOf('{{') + 2)) {
      const match = expression.match(/\{\{.*\{\{/);
      if (match) {
        errors.push('Nested expressions are not supported');
      }
    }

    // Check for empty expressions
    const emptyExpressionPattern = /\{\{\s*\}\}/;
    if (emptyExpressionPattern.test(expression)) {
      errors.push('Empty expression found');
    }

    return errors;
  }

  /**
   * Extract all expressions from a string
   */
  private static extractExpressions(text: string): string[] {
    const expressions: string[] = [];
    let match;
    
    while ((match = this.EXPRESSION_PATTERN.exec(text)) !== null) {
      expressions.push(match[1].trim());
    }
    
    return expressions;
  }

  /**
   * Validate a single expression content
   */
  private static validateSingleExpression(
    expr: string,
    context: ExpressionContext,
    result: ExpressionValidationResult
  ): void {
    // Check for $json usage
    let match;
    const jsonPattern = new RegExp(this.VARIABLE_PATTERNS.json.source, this.VARIABLE_PATTERNS.json.flags);
    while ((match = jsonPattern.exec(expr)) !== null) {
      result.usedVariables.add('$json');

      if (!context.hasInputData && !context.isInLoop) {
        result.warnings.push(
          'Using $json but node might not have input data'
        );
      }

      // Check for suspicious property names that might be test/invalid data
      const fullMatch = match[0];
      if (fullMatch.includes('.invalid') || fullMatch.includes('.undefined') ||
          fullMatch.includes('.null') || fullMatch.includes('.test')) {
        result.warnings.push(
          `Property access '${fullMatch}' looks suspicious - verify this property exists in your data`
        );
      }
    }

    // Check for $node references
    const nodePattern = new RegExp(this.VARIABLE_PATTERNS.node.source, this.VARIABLE_PATTERNS.node.flags);
    while ((match = nodePattern.exec(expr)) !== null) {
      const nodeName = match[1];
      result.usedNodes.add(nodeName);
      result.usedVariables.add('$node');
    }

    // Check for $input usage
    const inputPattern = new RegExp(this.VARIABLE_PATTERNS.input.source, this.VARIABLE_PATTERNS.input.flags);
    while ((match = inputPattern.exec(expr)) !== null) {
      result.usedVariables.add('$input');
      
      if (!context.hasInputData) {
        result.warnings.push(
          '$input is only available when the node has input data'
        );
      }
    }

    // Check for $items usage
    const itemsPattern = new RegExp(this.VARIABLE_PATTERNS.items.source, this.VARIABLE_PATTERNS.items.flags);
    while ((match = itemsPattern.exec(expr)) !== null) {
      const nodeName = match[1];
      result.usedNodes.add(nodeName);
      result.usedVariables.add('$items');
    }

    // Check for other variables
    for (const [varName, pattern] of Object.entries(this.VARIABLE_PATTERNS)) {
      if (['json', 'node', 'input', 'items'].includes(varName)) continue;
      
      const testPattern = new RegExp(pattern.source, pattern.flags);
      if (testPattern.test(expr)) {
        result.usedVariables.add(`$${varName}`);
      }
    }

    // Check for common mistakes
    this.checkCommonMistakes(expr, result);
  }

  /**
   * Check for common expression mistakes
   */
  private static checkCommonMistakes(
    expr: string,
    result: ExpressionValidationResult
  ): void {
    // Check for missing $ prefix - but exclude cases where $ is already present
    const missingPrefixPattern = /(?<!\$)\b(json|node|input|items|workflow|execution)\b(?!\s*:)/;
    if (expr.match(missingPrefixPattern)) {
      result.warnings.push(
        'Possible missing $ prefix for variable (e.g., use $json instead of json)'
      );
    }

    // Check for incorrect array access
    if (expr.includes('$json[') && !expr.match(/\$json\[\d+\]/)) {
      result.warnings.push(
        'Array access should use numeric index: $json[0] or property access: $json.property'
      );
    }

    // Check for Python-style property access
    if (expr.match(/\$json\['[^']+'\]/)) {
      result.warnings.push(
        "Consider using dot notation: $json.property instead of $json['property']"
      );
    }

    // Check for undefined/null access attempts
    if (expr.match(/\?\./)) {
      result.warnings.push(
        'Optional chaining (?.) is not supported in n8n expressions'
      );
    }

    // Check for template literals
    if (expr.includes('${')) {
      result.errors.push(
        'Template literals ${} are not supported. Use string concatenation instead'
      );
    }
  }

  /**
   * Check that all referenced nodes exist
   */
  private static checkNodeReferences(
    result: ExpressionValidationResult,
    context: ExpressionContext
  ): void {
    for (const nodeName of result.usedNodes) {
      if (!context.availableNodes.includes(nodeName)) {
        result.errors.push(
          `Referenced node "${nodeName}" not found in workflow`
        );
      }
    }
  }

  /**
   * Validate all expressions in a node's parameters
   */
  static validateNodeExpressions(
    parameters: any,
    context: ExpressionContext
  ): ExpressionValidationResult {
    const combinedResult: ExpressionValidationResult = {
      valid: true,
      errors: [],
      warnings: [],
      usedVariables: new Set(),
      usedNodes: new Set(),
    };

    const visited = new WeakSet();
    this.validateParametersRecursive(parameters, context, combinedResult, '', visited);
    
    combinedResult.valid = combinedResult.errors.length === 0;
    return combinedResult;
  }

  /**
   * Recursively validate expressions in parameters
   */
  private static validateParametersRecursive(
    obj: any,
    context: ExpressionContext,
    result: ExpressionValidationResult,
    path: string = '',
    visited: WeakSet<object> = new WeakSet()
  ): void {
    // Handle circular references
    if (obj && typeof obj === 'object') {
      if (visited.has(obj)) {
        return; // Skip already visited objects
      }
      visited.add(obj);
    }
    
    if (typeof obj === 'string') {
      if (obj.includes('{{')) {
        const validation = this.validateExpression(obj, context);
        
        // Add path context to errors
        validation.errors.forEach(error => {
          result.errors.push(path ? `${path}: ${error}` : error);
        });
        
        validation.warnings.forEach(warning => {
          result.warnings.push(path ? `${path}: ${warning}` : warning);
        });
        
        // Merge used variables and nodes
        validation.usedVariables.forEach(v => result.usedVariables.add(v));
        validation.usedNodes.forEach(n => result.usedNodes.add(n));
      }
    } else if (Array.isArray(obj)) {
      obj.forEach((item, index) => {
        this.validateParametersRecursive(
          item,
          context,
          result,
          `${path}[${index}]`,
          visited
        );
      });
    } else if (obj && typeof obj === 'object') {
      Object.entries(obj).forEach(([key, value]) => {
        const newPath = path ? `${path}.${key}` : key;
        this.validateParametersRecursive(value, context, result, newPath, visited);
      });
    }
  }
}