import fs from 'fs-extra';
import path from 'path';
import { glob } from 'glob';
import { Node, Parser, Tree } from 'web-tree-sitter';
import { Matcher, MatchResult } from './matcher.js';
import { IgnoreManager } from './ignore-manager.js';
import { WASMLoader } from './languages/wasm-loader.js';
import chalk from 'chalk';
import { Rule, PatternClause, ProjectContext } from '@scandiumsys/owstra-types';
import { TaintAnalyzer } from './taint-analyzer.js';
import { BooleanEvaluator } from './boolean-evaluator.js';
import { ProjectContextManager } from './project-context-manager.js';
import { JSIndexer } from './indexers/js-indexer.js';
import { PythonIndexer } from './indexers/python-indexer.js';
import { PHPIndexer } from './indexers/php-indexer.js';
import { JavaIndexer } from './indexers/java-indexer.js';
import { GoIndexer } from './indexers/go-indexer.js';
import { RubyIndexer } from './indexers/ruby-indexer.js';
import { JSResolver } from './resolvers/js-resolver.js';
import { TypeScriptIndexer } from './indexers/typescript-indexer.js';
import { KotlinIndexer } from './indexers/kotlin-indexer.js';
import { SwiftIndexer } from './indexers/swift-indexer.js';
import { CSharpIndexer } from './indexers/csharp-indexer.js';

interface Match {
  node: Node;
  bindings: Record<string, Node>;
}

export class Engine {
  private onProgress?: (percentage: number, file: string) => void;
  private onFinding?: (finding: any) => void;
  private currentFindings: any[] = [];
  private matcher: Matcher;
  private taintAnalyzer: TaintAnalyzer;
  private ignoreManager!: IgnoreManager;
  private workspacePath: string;
  private strictness: 'low' | 'normal' | 'high' = 'normal';
  private stats = {
    findings: 0,
    filesScanned: 0,
    filesIgnored: 0,
    rulesRun: 0,
    totalFilesFound: 0,
  };
  private projectContext: ProjectContextManager;
  private patternCache = new Map<string, Tree>();
  private parser: Parser | null = null;

  constructor(workspacePath: string, options?: {
    onProgress?: (percentage: number, file: string) => void,
    onFinding?: (finding: any) => void
  }) {
    this.workspacePath = workspacePath;
    this.matcher = new Matcher();
    this.projectContext = new ProjectContextManager(workspacePath);

    // Register default providers
    this.projectContext.registerIndexer(new JSIndexer());
    this.projectContext.registerIndexer(new PythonIndexer());
    this.projectContext.registerIndexer(new PHPIndexer());
    this.projectContext.registerIndexer(new JavaIndexer());
     this.projectContext.registerIndexer(new GoIndexer());
    this.projectContext.registerIndexer(new RubyIndexer());
    this.projectContext.registerIndexer(new TypeScriptIndexer());
    this.projectContext.registerIndexer(new KotlinIndexer());
    this.projectContext.registerIndexer(new SwiftIndexer());
    this.projectContext.registerIndexer(new CSharpIndexer());
    
    this.projectContext.registerResolver(new JSResolver(workspacePath));

    this.taintAnalyzer = new TaintAnalyzer(this.projectContext, this.matcher);
    // IgnoreManager moved to scan() to support dynamic depth
    this.onProgress = options?.onProgress;
    this.onFinding = options?.onFinding;
  }

  public async scan(
    scanPath: string,
    rules: Rule[],
    strictness: 'low' | 'normal' | 'high' = 'normal',
    depth: 'normal' | 'all' = 'normal'
  ) {
    this.strictness = strictness;
    this.ignoreManager = new IgnoreManager(this.workspacePath, depth);
    await WASMLoader.init();

    let files: string[] = [];
    if (fs.statSync(scanPath).isFile()) {
      files = [path.resolve(scanPath)];
    } else {
      files = glob.sync('**/*', {
        cwd: scanPath,
        nodir: true,
        absolute: true,
      });
    }

    // 1. Perform Indexing Pass (New)
    console.log(chalk.blue('Building project index for deep analysis...'));
    await this.performIndexing(files);

    // Reset stats
    this.stats = { findings: 0, filesScanned: 0, filesIgnored: 0, rulesRun: rules.length, totalFilesFound: files.length };

    const filteredFiles = files.filter(file => {
      const relativePath = path.relative(scanPath, file);
      const ignored = this.ignoreManager.shouldIgnore(relativePath);
      if (ignored) this.stats.filesIgnored++;
      return !ignored;
    });

    console.log(`Scanning ${filteredFiles.length} files...`);

    for (let i = 0; i < filteredFiles.length; i++) {
      const file = filteredFiles[i]!;
      const ext = path.extname(file).slice(1);
      const lang = this.mapExtensionToLanguage(ext);

      if (this.onProgress) {
        const percentage = Math.round(((i + 1) / filteredFiles.length) * 100);
        this.onProgress(percentage, file);
      }

      if (!lang) continue;

      const fileRules = rules.filter(r => r.languages.includes(lang));
      if (fileRules.length === 0) continue;

      try {
        await this.scanFile(file, lang, fileRules);
        this.stats.filesScanned++;
      } catch (err: any) {
        console.error(chalk.red(`  ⚠️  Failed to scan ${path.basename(file)}: ${err.message}`));
      }
    }

    this.cleanup();
    return { ...this.stats, findingsList: this.currentFindings };
  }

  public cleanup() {
    // Free WASM memory for all cached patterns
    for (const tree of this.patternCache.values()) {
      tree.delete();
    }
    this.patternCache.clear();
    this.taintAnalyzer.cleanup();
  }

  private async performIndexing(files: string[]) {
    if (!this.parser) this.parser = new Parser();
    const parser = this.parser;

    for (const file of files) {
      const ext = path.extname(file).slice(1);
      const langName = this.mapExtensionToLanguage(ext);
      if (!langName) continue;

      const relativePath = path.relative(this.workspacePath, file);
      if (this.ignoreManager.shouldIgnore(relativePath)) continue;

      try {
        const lang = await WASMLoader.getLanguage(langName);
        parser.setLanguage(lang);
        const content = fs.readFileSync(file, 'utf-8');
        const tree = parser.parse(content);
        if (!tree) continue;

        try {
          // This is a bit slow as we parse everything twice (once for index, once for rules)
          // Optimization: Cache trees or use a more efficient indexer
          const indexer = this.projectContext.getIndexer(langName);
          if (indexer) {
            const exports = indexer.extractExports(tree.rootNode, file);
            this.projectContext.addExports(langName, exports);

            const imports = indexer.extractImports(tree.rootNode);
            this.projectContext.addImports(file, imports);

            const calls = indexer.extractCalls(tree.rootNode);
            for (const call of calls) {
              this.projectContext.addCallSite(call.name, file, call.argPositions);
            }
          }
        } finally {
          tree.delete();
        }
      } catch (e) {
        console.log("Indexing error: ", e)
        // Skip indexing errors for individual files
      }
    }
  }

  private async scanFile(filePath: string, langName: string, rules: Rule[]) {
    const content = fs.readFileSync(filePath, 'utf-8');
    const lang = await WASMLoader.getLanguage(langName);
    if (!this.parser) this.parser = new Parser();
    const parser = this.parser;
    parser.setLanguage(lang);

    const tree = parser.parse(content)!;

    try {
      for (const rule of rules) {
        if (!this.shouldRunRule(rule)) continue;
        await this.evaluateRule(tree.rootNode, rule, filePath, parser, langName);
      }
    } finally {
      tree.delete(); // Free source code tree memory
    }
  }

  private shouldRunRule(rule: Rule): boolean {
    if (this.strictness === 'low') {
      return rule.severity === 'ERROR';
    }
    if (this.strictness === 'normal') {
      return rule.severity === 'ERROR' || rule.severity === 'WARNING';
    }
    return true; // 'high' shows everything
  }

  private async evaluateRule(root: Node, rule: Rule, filePath: string, parser: Parser, langName: string) {
    if (rule.mode === 'taint') {
      await this.evaluateTaintRule(root, rule, filePath, parser, langName);
      return;
    }

    // 1. Identify the "Anchor" pattern
    const initialPatternText = this.findAnchorPattern(rule);
    if (!initialPatternText) return;

    // 2. Find candidate matches for the anchor pattern
    const candidates = this.getMatches(root, initialPatternText, parser, langName);

    // 3. Filter candidates through the full rule condition tree
    const matches: Match[] = [];
    for (const m of candidates) {
      const res = await this.evaluatePatternClause(m.node, rule, m.bindings, parser, langName, rule);
      if (res.isMatch) matches.push(m);
    }

    // 4. Report remaining matches
    for (const m of matches) {
      this.stats.findings++;
      this.reportMatch(rule, m.node, filePath, m.bindings);
    }
  }

  private async evaluateTaintRule(root: Node, rule: Rule, filePath: string, parser: Parser, langName: string) {
    if (!rule['pattern-sinks']) return;

    for (const sinkClause of rule['pattern-sinks']) {
      const anchor = this.findAnchorPattern(sinkClause);
      if (!anchor) continue;

      const candidates = this.getMatches(root, anchor, parser, langName);
      for (const m of candidates) {
        // Evaluate the sink clause fully (in case it has pattern-inside, etc.)
        const sinkRes = await this.evaluatePatternClause(m.node, sinkClause, m.bindings, parser, langName, rule);
        if (!sinkRes.isMatch) continue;

        // Trace taint labels from the matched node
        const activeLabels = await this.taintAnalyzer.getTaintLabels(m.node, langName, rule, parser);
        
        // Evaluate requirements
        const requires = sinkClause.requires;
        if (BooleanEvaluator.evaluate(requires, activeLabels)) {
          if (requires || activeLabels.size > 0) {
            this.stats.findings++;
            this.reportMatch(rule, m.node, filePath, sinkRes.bindings);
          }
        }
      }
    }
  }

  private async evaluatePatternClause(
    node: Node,
    clause: PatternClause,
    bindings: Record<string, Node>,
    parser: Parser,
    langName: string,
    ruleContext: Rule
  ): Promise<MatchResult> {
    let currentBindings = { ...bindings };

    // 1. Handle 'pattern' (AND)
    if (clause.pattern) {
      const results = await this.matchSinglePattern(node, clause.pattern, parser, langName, ruleContext);
      if (results.length === 0) return { isMatch: false, bindings: {} };

      // Try to find a binding set that merges with existing ones
      let found = false;
      for (const res of results) {
        const merged = this.matcher.mergeBindings(currentBindings, res.bindings);
        if (merged) {
          currentBindings = merged;
          found = true;
          break;
        }
      }
      if (!found) return { isMatch: false, bindings: {} };
    }

    // 2. Handle 'pattern-inside' (Context check)
    if (clause['pattern-inside']) {
      const results = await this.evaluatePatternInside(node, clause['pattern-inside'], currentBindings, parser, langName, ruleContext);
      if (results.length === 0) return { isMatch: false, bindings: {} };
      // Take the first valid binding set
      currentBindings = results[0]!.bindings;
    }

    // 3. Handle 'pattern-not-inside'
    if (clause['pattern-not-inside']) {
      const results = await this.evaluatePatternInside(node, clause['pattern-not-inside'], currentBindings, parser, langName, ruleContext);
      if (results.length > 0) return { isMatch: false, bindings: {} };
    }

    // 4. Handle 'patterns' (AND - Recursive)
    if (clause.patterns) {
      for (const subClause of clause.patterns) {
        const normalizedClause = typeof subClause === 'string' ? { pattern: subClause } : subClause;
        const res = await this.evaluatePatternClause(node, normalizedClause, currentBindings, parser, langName, ruleContext);
        if (!res.isMatch) return { isMatch: false, bindings: {} };
        currentBindings = res.bindings;
      }
    }

    // 5. Handle 'pattern-either' (OR - Recursive)
    if (clause['pattern-either']) {
      let matchedEither = false;
      let eitherBindings = currentBindings;
      for (const subClause of clause['pattern-either']) {
        const normalizedClause = typeof subClause === 'string' ? { pattern: subClause } : subClause;
        const res = await this.evaluatePatternClause(node, normalizedClause, currentBindings, parser, langName, ruleContext);
        if (res.isMatch) {
          matchedEither = true;
          eitherBindings = res.bindings;
          break;
        }
      }
      if (!matchedEither) return { isMatch: false, bindings: {} };
      currentBindings = eitherBindings;
    }

    // 6. Handle 'pattern-not'
    if (clause['pattern-not']) {
      const negativePatterns = Array.isArray(clause['pattern-not']) ? clause['pattern-not'] : [clause['pattern-not']];
      for (const p of negativePatterns) {
        const results = await this.matchSinglePattern(node, p, parser, langName, ruleContext);
        if (results.length > 0) return { isMatch: false, bindings: {} };
      }
    }

    // 7. Handle 'metavariable-regex'
    if (clause['metavariable-regex']) {
      const { metavariable, regex } = clause['metavariable-regex'];
      if (!this.matcher.checkMetavariableRegex(metavariable, regex, currentBindings)) {
        return { isMatch: false, bindings: {} };
      }
    }

    // 8. Taint Analysis Verification (Rule level for now)
    const anyClause = clause as any;
    if (anyClause['taint-check']) {
      for (const varWithDollar of anyClause['taint-check']) {
        const usageNode = currentBindings[varWithDollar];
        if (usageNode) {
          const labels = await this.taintAnalyzer.getTaintLabels(usageNode, langName, ruleContext, parser);
          if (labels.size === 0) {
            return { isMatch: false, bindings: {} };
          }
        }
      }
    }

    return { isMatch: true, bindings: currentBindings };
  }

  private async evaluatePatternInside(
    node: Node,
    pattern: string | PatternClause,
    bindings: Record<string, Node>,
    parser: Parser,
    langName: string,
    ruleContext: Rule
  ): Promise<MatchResult[]> {
    const results: MatchResult[] = [];
    let current = node.parent;
    while (current) {
      const matches = await this.matchSinglePattern(current, pattern, parser, langName, ruleContext);
      for (const res of matches) {
        const merged = this.matcher.mergeBindings(bindings, res.bindings);
        if (merged) results.push({ isMatch: true, bindings: merged });
      }
      current = current.parent;
    }

    return results;
  }
  private getPatternNode(patternStr: string, parser: Parser, langName: string): Node | null {
    let p = patternStr;
    if (langName === 'php' && !p.includes('<?php')) {
      p = `<?php ${p}`;
      if (!p.endsWith(';') && !p.endsWith('}')) p += ';';
    }

    if (langName === 'go' || langName === 'csharp') {
      // Both Go and C# have trouble with $ and ... in some snippet contexts.
      p = p.replace(/\$([A-Z_][A-Z0-9_]*)/g, 'OWSTRA_VAR_$1');
      p = p.replace(/\.\.\./g, 'OWSTRA_ELLIPSIS');
      
      if (langName === 'go') {
        p = `func _() { ${p} }`;
      } else if (langName === 'csharp') {
        // Detect whether the pattern is class-level or expression-level.
        // Class-level patterns (class declarations, using directives, etc.) go in class body.
        // Expression-level patterns (method calls, assignments, etc.) go inside a method body
        // so tree-sitter correctly parses them as invocation_expression, assignment_expression, etc.
        // instead of field_declaration (which would cause false matches against every node).
        const isClassLevel = p.includes('class ') || p.includes('using ') ||
                             p.includes('namespace ') || p.includes('interface ') ||
                             p.includes('public ') || p.includes('private ') || 
                             p.includes('protected ') || p.includes('internal ');
        if (!isClassLevel) {
          p = `class OwstraWrapper { void OwstraMethod() { ${p} } }`;
        }
      }
    }

    const cacheKey = `${langName}:${p}`;
    let tree: Tree | null | undefined = this.patternCache.get(cacheKey);

    if (!tree) {
      tree = parser.parse(p);
      if (!tree) return null;
      this.patternCache.set(cacheKey, tree);
    }

    return this.getActualPatternNode(tree.rootNode);
  }

  private findAnchorPattern(clause: PatternClause | string): string | undefined {
    if (typeof clause === 'string') return clause;
    if (clause.pattern) return clause.pattern;
    if (clause.patterns) {
      for (const p of clause.patterns) {
        const res = this.findAnchorPattern(p);
        if (res) return res;
      }
    }
    if (clause['pattern-either']) {
      for (const p of clause['pattern-either']) {
        const res = this.findAnchorPattern(p);
        if (res) return res;
      }
    }
    return undefined;
  }

  private async matchSinglePattern(
    node: Node, 
    pattern: string | PatternClause, 
    parser: Parser, 
    langName: string,
    ruleContext: Rule
  ): Promise<MatchResult[]> {
    if (typeof pattern !== 'string') {
      const res = await this.evaluatePatternClause(node, pattern, {}, parser, langName, ruleContext);
      return res.isMatch ? [res] : [];
    }

    const actualPatternNode = this.getPatternNode(pattern, parser, langName);
    if (!actualPatternNode) return [];

    return this.matcher.match(actualPatternNode, node);
  }

  private getMatches(root: Node, patternStr: string, parser: Parser, langName: string): Match[] {
    const actualPatternNode = this.getPatternNode(patternStr, parser, langName);
    if (!actualPatternNode) return [];

    const matches: Match[] = [];
    const stack: Node[] = [root];

    while (stack.length > 0) {
      const node = stack.pop()!;
      const results = this.matcher.match(actualPatternNode, node);
      for (const res of results) {
        matches.push({ node, bindings: res.bindings });
      }
      stack.push(...node.children);
    }
    return matches;
  }

  private searchPattern(root: Node, patternRoot: Node, rule: Rule, filePath: string) {
    // This is a naive search: try to match pattern at every node in the tree
    const stack: Node[] = [root];

    // We want to match the "content" of the pattern root, not necessarily the root itself if it's a program
    // Many parsers wrap everything in a "program" or "module" node
    const actualPatternNode = this.getActualPatternNode(patternRoot);

    while (stack.length > 0) {
      const node = stack.pop()!;

      const results = this.matcher.match(actualPatternNode, node);
      for (const res of results) {
        this.stats.findings++;
        this.reportMatch(rule, node, filePath, res.bindings);
      }

      stack.push(...node.children);
    }
  }

  private getActualPatternNode(patternRoot: Node): Node {
    let current = patternRoot;
    const skipTypes = [
      'program', 
      'expression_statement', 
      'statement', 
      'php_tag',
      'compilation_unit', 
      'source_file',      
      'module',           
      'function_declaration',
      'block',
      'statement_list',
      'global_statement',
      'class_declaration',
      'declaration_list'
    ];

    while (current.childCount > 0) {
      const isOwstraWrapper = current.type === 'class_declaration' && current.childForFieldName('name')?.text === 'OwstraWrapper';
      const isOwstraMethod = current.type === 'method_declaration' && current.childForFieldName('name')?.text === 'OwstraMethod';

      if (!skipTypes.includes(current.type) && !isOwstraWrapper && !isOwstraMethod) {
        break;
      }

      // Special handling for Go's function wrapper or C#'s class/method wrapper
      if (current.type === 'function_declaration' || isOwstraWrapper || isOwstraMethod) {
          const body = current.childForFieldName('body');
          if (body) {
              current = body;
              continue;
          }
      }

      const namedChildren = current.children.filter(c => c.isNamed && c.type !== 'php_tag');
      if (namedChildren.length === 1) {
        current = namedChildren[0]!;
      } else {
        break;
      }
    }
    return current;
  }

  private reportMatch(rule: Rule, node: Node, filePath: string, bindings: Record<string, Node>) {
    let message = rule.message;
    let fix = rule.fix;

    // Replace metavariables in message and fix
    for (const [key, valueNode] of Object.entries(bindings)) {
      const value = valueNode.text;
      message = message.replace(new RegExp(`\\${key}`, 'g'), value);
      if (fix) fix = fix.replace(new RegExp(`\\${key}`, 'g'), value);
    }

    const severityColor = rule.severity === 'ERROR' ? chalk.red : rule.severity === 'WARNING' ? chalk.yellow : chalk.blue;
    const severityPrefix = severityColor.bold(`[${rule.severity}]`);

    console.log(`\n${severityPrefix} ${chalk.bold(rule.id)}`);
    console.log(`  ${chalk.gray('File:')} ${chalk.cyan(filePath)}:${chalk.yellow(node.startPosition.row + 1)}`);
    console.log(`  ${chalk.gray('Message:')} ${message}`);

    if (fix) {
      console.log(`  ${chalk.green('💡 Fix:')} ${chalk.white(fix)}`);
    }

    const codeLine = node.text.split('\n')[0];
    console.log(`  ${chalk.gray('Code:')} ${chalk.italic(codeLine?.trim())}`);

    const finding = {
      ruleId: rule.id,
      message,
      severity: rule.severity,
      filePath,
      line: node.startPosition.row + 1,
      code: codeLine?.trim() || '',
      fix
    };

    this.currentFindings.push(finding);
    if (this.onFinding) this.onFinding(finding);
  }

  private mapExtensionToLanguage(ext: string): string | null {
    const maps: Record<string, string> = {
      ts: 'typescript',
      tsx: 'typescript',
      mts: 'typescript',
      cts: 'typescript',
      js: 'javascript',
      jsx: 'javascript',
      mjs: 'javascript',
      cjs: 'javascript',
      py: 'python',
      rb: 'ruby',
      go: 'go',
      java: 'java',
      c: 'c',
      cpp: 'cpp',
      php: 'php',
      phtml: 'php',
      blade: 'php',
      swift: 'swift',
      kt: 'kotlin',
      kts: 'kotlin',
      dart: 'dart',
      m: 'objc',
      h: 'objc',
      cs: 'csharp',
    };
    return maps[ext.toLowerCase()] || null;
  }
}
