import { Node } from 'web-tree-sitter';

export interface MatchResult {
  isMatch: boolean;
  bindings: Record<string, Node>;
}

export class Matcher {
  /**
   * Matches a pattern node against a source node.
   * Returns an array of MatchResults with bindings if successful.
   */
  public match(patternNode: Node, sourceNode: Node): MatchResult[] {
    const varName = this.isMetavariable(patternNode);

    // Handle Metavariable: $X matches anything and captures it
    if (varName) {
      return [{ isMatch: true, bindings: { [varName]: sourceNode } }];
    }

    // Handle String Wildcard: "..." matches any string literal
    if (this.isStringWildcard(patternNode)) {
        const type = sourceNode.type;
        if (type === 'string' || type === 'string_literal' || type === 'template_string') {
            return [{ isMatch: true, bindings: {} }];
        }
    }

    // Type must match (e.g., call_expression)
    if (patternNode.type !== sourceNode.type) {
      // C# Compatibility: local_function_statement in patterns often matches method_declaration in source
      const isMethodCompat = (
        (patternNode.type === 'local_function_statement' && sourceNode.type === 'method_declaration') ||
        (patternNode.type === 'global_statement' && sourceNode.type === 'method_declaration') ||
        (patternNode.type === 'expression_statement' && sourceNode.type === 'assignment_expression')
      );
      
      if (!isMethodCompat) return [];
    }

    // If it's a leaf node (no children), text must match
    if (patternNode.childCount === 0) {
      if (patternNode.text === sourceNode.text) {
        return [{ isMatch: true, bindings: {} }];
      } else {
        return [];
      }
    }

    // Recursively match children
    return this.matchChildren(patternNode, sourceNode);
  }

  private isMetavariable(node: Node): string | null {
    const text = node.text;
    if (/^\$[A-Z_][A-Z0-9_]*$/.test(text)) {
      if (node.childCount === 0) return text;
      
      // For languages like PHP where variable_name is a parent of $ and name
      if (node.type === 'variable_name' || node.type === 'identifier') {
        return text;
      }
    }
    
    // Support transformed Go/C# metavariables
    if (/^OWSTRA_VAR_[A-Z0-9_]+$/.test(text)) {
      return '$' + text.replace('OWSTRA_VAR_', '');
    }
    
    return null;
  }

  private matchChildren(patternNode: Node, sourceNode: Node): MatchResult[] {
    const patternChildren = patternNode.children;
    const sourceChildren = sourceNode.children;

    return this.matchRecursive(patternChildren, sourceChildren, 0, 0, {});
  }

  private matchRecursive(
    pChildren: Node[],
    sChildren: Node[],
    pIdx: number,
    sIdx: number,
    currentBindings: Record<string, Node>
  ): MatchResult[] {
    // Base Case: Both patterns and source exhausted
    if (pIdx === pChildren.length) {
      if (sIdx === sChildren.length) {
        return [{ isMatch: true, bindings: currentBindings }];
      }
      return [];
    }

    const pNode = pChildren[pIdx]!;

    // Handle Ellipsis
    if (this.isEllipsis(pNode)) {
      const results: MatchResult[] = [];
      const nextPIdx = pIdx + 1;

      // Ellipsis matches zero or more elements
      for (let i = sIdx; i <= sChildren.length; i++) {
        const subResults = this.matchRecursive(pChildren, sChildren, nextPIdx, i, { ...currentBindings });
        results.push(...subResults);
        
        // Specially handle trailing or leading commas if ellipsis matches zero
        if (i === sIdx && pIdx + 1 < pChildren.length && pChildren[pIdx + 1].text === ',') {
            const skipCommaResults = this.matchRecursive(pChildren, sChildren, pIdx + 2, sIdx, { ...currentBindings });
            results.push(...skipCommaResults);
        }
        if (i === sIdx && pIdx > 0 && pChildren[pIdx - 1].text === ',') {
            // Already handled in a previous call? This is recursive.
        }
      }
      return results;
    }

    // Normal matching
    if (sIdx >= sChildren.length) return [];

    const matches = this.match(pNode, sChildren[sIdx]!);
    const results: MatchResult[] = [];

    // Allow skipping a comma if it's followed by an ellipsis that matches zero
    if (pNode.text === ',' && pIdx + 1 < pChildren.length && this.isEllipsis(pChildren[pIdx + 1])) {
      const skipBoth = this.matchRecursive(pChildren, sChildren, pIdx + 2, sIdx, currentBindings);
      results.push(...skipBoth);
    }

    for (const m of matches) {
      const merged = this.mergeBindings(currentBindings, m.bindings);
      if (merged) {
        results.push(...this.matchRecursive(pChildren, sChildren, pIdx + 1, sIdx + 1, merged));
      }
    }

    // Flexible matching: If pNode didn't match sChildren[sIdx], or even if it did, 
    // we can try skipping sChildren[sIdx] and matching pNode against the next source child.
    // This allows patterns like 'using A; ...' to match even if 'using A;' is not the first line.
    // We only do this if results.length is 0 (to find the first match) or if we want to be exhaustive.
    // To avoid over-matching, we only allow skipping source children that are top-level types.
    // IMPORTANT: Use exact type equality (Set.has), NOT substring matching (.includes),
    // to prevent types like 'expression_statement' or 'local_declaration_statement' from matching.
    const skippableTypes = new Set([
      'using_directive', 'class_declaration', 'method_declaration',
      'statement', 'declaration', 'attribute_list',
      'global_statement', 'namespace_declaration',
      'field_declaration', 'property_declaration',
      'constructor_declaration', 'enum_declaration',
      'interface_declaration', 'struct_declaration',
    ]);
    if (results.length === 0 && (skippableTypes.has(pNode.type) || skippableTypes.has(sChildren[sIdx]!.type))) {
        results.push(...this.matchRecursive(pChildren, sChildren, pIdx, sIdx + 1, currentBindings));
    }

    return results;
  }

  /**
   * Merges two binding sets, ensuring consistency.
   * Returns null if there is a conflict.
   */
  public mergeBindings(b1: Record<string, Node>, b2: Record<string, Node>): Record<string, Node> | null {
    const merged = { ...b1 };
    for (const [key, node] of Object.entries(b2)) {
      if (merged[key] && merged[key].text !== node.text) {
        return null; // Conflict
      }
      merged[key] = node;
    }
    return merged;
  }

  /**
   * Checks if a metavariable in the bindings matches a regex.
   */
  public checkMetavariableRegex(metavariable: string, regex: string, bindings: Record<string, Node>): boolean {
    const node = bindings[metavariable];
    if (!node) return false;
    
    try {
      let finalRegex = regex;
      let flags = '';
      if (regex.includes('(?i)')) {
        finalRegex = regex.replace('(?i)', '');
        flags += 'i';
      }
      const re = new RegExp(finalRegex, flags);
      return re.test(node.text);
    } catch (e) {
      console.error(`Invalid regex: ${regex}`);
      return false;
    }
  }

  private isEllipsis(node: Node): boolean {
    const text = node.text.trim();
    return text === '...' || text === 'OWSTRA_ELLIPSIS' || 
           (node.type === 'ERROR' && (text === '...' || text === 'OWSTRA_ELLIPSIS'));
  }

  private isStringWildcard(node: Node): boolean {
    return node.text === '"..."' || node.text === "'...'";
  }
}
