import fs from "node:fs";
import path from "node:path";
import { createRequire } from "node:module";
import { fileURLToPath, pathToFileURL } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const RAW_SQL_METHODS = new Set([
  "execute",
  "executemany",
  "query",
  "raw",
  "sql",
  "text",
  "from_statement",
]);

const BUILDER_METHODS = new Set([
  "query",
  "select",
  "select_from",
  "from",
  "table",
  "where",
  "filter",
  "filter_by",
  "andwhere",
  "orwhere",
  "wherein",
  "wheredate",
  "wherebetween",
  "eq",
  "neq",
  "lt",
  "lte",
  "gt",
  "gte",
  "match",
  "join",
  "leftjoin",
  "rightjoin",
  "innerjoin",
  "outerjoin",
  "order_by",
  "orderby",
  "order",
  "group_by",
  "groupby",
]);

function parseArgs(argv) {
  const args = {};
  for (let i = 0; i < argv.length; i += 1) {
    const token = argv[i];
    if (!token.startsWith("--")) {
      continue;
    }
    const key = token.slice(2);
    const next = argv[i + 1];
    if (!next || next.startsWith("--")) {
      args[key] = true;
    } else {
      args[key] = next;
      i += 1;
    }
  }
  return args;
}

function uniquePaths(items) {
  const seen = new Set();
  const out = [];
  for (const item of items) {
    const resolved = path.resolve(item);
    if (seen.has(resolved)) continue;
    seen.add(resolved);
    out.push(resolved);
  }
  return out;
}

function moduleSearchRoots(grammarDir) {
  const roots = [grammarDir, path.dirname(grammarDir), process.cwd()];
  let cursor = path.resolve(process.cwd());
  while (true) {
    const parent = path.dirname(cursor);
    if (parent === cursor) break;
    roots.push(parent);
    cursor = parent;
  }
  return uniquePaths(roots);
}

async function loadTreeSitterModule(grammarDir) {
  const bundledRuntimePath = path.join(__dirname, "vendor", "tree-sitter.js");
  if (fs.existsSync(bundledRuntimePath)) {
    return {
      module: await import(pathToFileURL(bundledRuntimePath).href),
      runtimeDir: path.dirname(bundledRuntimePath),
    };
  }

  for (const root of moduleSearchRoots(grammarDir)) {
    try {
      const requireFrom = createRequire(path.join(root, "__auraindex__.cjs"));
      let resolved;
      try {
        resolved = requireFrom.resolve("web-tree-sitter/tree-sitter.js");
      } catch {
        resolved = requireFrom.resolve("web-tree-sitter");
      }
      return {
        module: await import(pathToFileURL(resolved).href),
        runtimeDir: findRuntimeDirForTreeSitter(resolved),
      };
    } catch {
      // try next root
    }
  }

  throw new Error(
    "Cannot find package 'web-tree-sitter'. Install dependencies with: " +
      "npm install web-tree-sitter tree-sitter-python tree-sitter-javascript tree-sitter-typescript tree-sitter-php"
  );
}

function findRuntimeDirForTreeSitter(modulePath) {
  const start = path.dirname(modulePath);
  const searchDirs = [start, path.dirname(start), path.dirname(path.dirname(start))];
  for (const dir of uniquePaths(searchDirs)) {
    if (fs.existsSync(path.join(dir, "tree-sitter.wasm"))) {
      return dir;
    }
  }
  return start;
}

function normalizeSql(text) {
  return String(text || "").replace(/\s+/g, " ").trim();
}

function looksLikeSql(text) {
  const normalized = normalizeSql(text).toLowerCase();
  if (!normalized || normalized.length < 16) return false;
  if (normalized.includes("<") || normalized.includes("</")) return false;
  if (normalized.includes(" function ")) return false;
  if (normalized.startsWith("select ")) return normalized.includes(" from ");
  if (normalized.startsWith("insert ")) return normalized.includes(" into ");
  if (normalized.startsWith("update ")) return normalized.includes(" set ");
  if (normalized.startsWith("delete ")) return normalized.includes(" from ");
  if (normalized.startsWith("with ")) return normalized.includes(" select ") && normalized.includes(" from ");
  if (normalized.startsWith("replace ")) return normalized.includes(" into ");
  if (normalized.startsWith("merge ")) return normalized.includes(" into ");
  if (normalized.startsWith("create "))
    return /\b(table|index|view|materialized|schema|database|extension)\b/.test(normalized);
  if (normalized.startsWith("alter ")) return /\b(table|index|view)\b/.test(normalized);
  if (normalized.startsWith("drop ")) return /\b(table|index|view|schema|database)\b/.test(normalized);
  return false;
}

function normalizeIdentifier(value) {
  return String(value || "")
    .trim()
    .replace(/[`"' ]/g, "")
    .replace(/[^a-zA-Z0-9_.]/g, "_")
    .toLowerCase();
}

function camelToSnake(value) {
  return String(value || "")
    .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
    .replace(/[^a-zA-Z0-9_]/g, "_")
    .toLowerCase();
}

function pluralize(word) {
  if (!word) return word;
  if (word.endsWith("y") && !/[aeiou]y$/i.test(word)) return `${word.slice(0, -1)}ies`;
  if (word.endsWith("s")) return word;
  return `${word}s`;
}

function classToTable(className) {
  const snake = normalizeIdentifier(camelToSnake(className));
  return pluralize(snake);
}

function unquoteLiteral(token) {
  if (!token) return "";
  const trimmed = token.trim();

  const tripleSingle = trimmed.match(/^[rRuUbBfF]{0,3}'''([\s\S]*)'''$/);
  if (tripleSingle) return tripleSingle[1].replace(/\{[^{}]+\}/g, "?");

  const tripleDouble = trimmed.match(/^[rRuUbBfF]{0,3}"""([\s\S]*)"""$/);
  if (tripleDouble) return tripleDouble[1].replace(/\{[^{}]+\}/g, "?");

  const quoted = trimmed.match(/^[rRuUbBfF]{0,3}(['"`])([\s\S]*)\1$/);
  if (quoted) {
    let body = quoted[2];
    body = body.replace(/\$\{[^}]+\}/g, "?");
    body = body.replace(/\{[^{}]+\}/g, "?");
    return body;
  }
  return trimmed;
}

function extractStringLiterals(text) {
  const literals = [];
  const regex =
    /[rRuUbBfF]{0,3}(?:'''[\s\S]*?'''|"""[\s\S]*?"""|'(?:\\.|[^'])*'|"(?:\\.|[^"])*"|`[\s\S]*?`)/g;
  let match;
  while ((match = regex.exec(text)) !== null) {
    const value = normalizeSql(unquoteLiteral(match[0]));
    if (value) literals.push(value);
  }
  return literals;
}

function extractMethodChain(callText) {
  const methods = [];
  const dotted = /(?:\.|->|::)([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
  let match;
  while ((match = dotted.exec(callText)) !== null) {
    methods.push(match[1].toLowerCase());
  }
  const root = callText.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
  if (root) methods.unshift(root[1].toLowerCase());
  return [...new Set(methods)];
}

function extractRawSqlFromCallText(callText) {
  const methods = extractMethodChain(callText);
  if (!methods.some((method) => RAW_SQL_METHODS.has(method))) return null;

  const literals = extractStringLiterals(callText);
  for (const literal of literals) {
    if (looksLikeSql(literal)) {
      return literal;
    }
  }
  return null;
}

function inferBaseTable(callText) {
  const explicitPatterns = [
    /(?:\.|->|::)(?:from|table)\(\s*['"`]([A-Za-z0-9_.-]+)['"`]/i,
    /\b(?:from|table)\(\s*['"`]([A-Za-z0-9_.-]+)['"`]/i,
  ];
  for (const pattern of explicitPatterns) {
    const match = callText.match(pattern);
    if (match) {
      const table = normalizeIdentifier(match[1]);
      if (table) return table;
    }
  }

  const ormPatterns = [
    /\b(?:query|select|select_from)\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*[,\)]/i,
    /(?:\.|->|::)(?:query|select|select_from)\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*[,\)]/i,
    /\b([A-Za-z_][A-Za-z0-9_]*)::(?:query|select|where|orderBy|groupBy)\s*\(/i,
  ];
  for (const pattern of ormPatterns) {
    const match = callText.match(pattern);
    if (match) {
      const table = classToTable(match[1]);
      if (table) return table;
    }
  }

  return null;
}

function extractColumnToken(rawToken, defaultTable) {
  const token = String(rawToken || "").trim();
  if (!token) return null;

  const quoted = token.match(/^['"`]([^'"`]+)['"`]$/);
  if (quoted) {
    const cleaned = normalizeIdentifier(quoted[1]);
    if (!cleaned) return null;
    if (cleaned.includes(".")) return cleaned;
    return defaultTable ? `${defaultTable}.${cleaned}` : cleaned;
  }

  const attr = token.match(/([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)/);
  if (attr) {
    const left = attr[1];
    const table = /^[A-Z]/.test(left) ? classToTable(left) : normalizeIdentifier(left);
    const col = normalizeIdentifier(attr[2]);
    if (table && col) return `${table}.${col}`;
  }

  const kwArg = token.match(/([A-Za-z_][A-Za-z0-9_]*)\s*=/);
  if (kwArg) {
    const col = normalizeIdentifier(kwArg[1]);
    if (col) return defaultTable ? `${defaultTable}.${col}` : col;
  }

  const simple = token.match(/^([A-Za-z_][A-Za-z0-9_]*)$/);
  if (simple) {
    const col = normalizeIdentifier(simple[1]);
    if (col) return defaultTable ? `${defaultTable}.${col}` : col;
  }

  return null;
}

function extractColumnsByPattern(callText, regex, defaultTable) {
  const cols = [];
  let match;
  while ((match = regex.exec(callText)) !== null) {
    const rawArg = String(match[1] || "").split(",")[0].trim();
    const col = extractColumnToken(rawArg, defaultTable);
    if (col) cols.push(col);
  }
  return [...new Set(cols)];
}

function extractJoinTables(callText) {
  const tables = [];
  const regex = /(?:\.|->|::)(?:join|leftjoin|rightjoin|innerjoin|outerjoin)\s*\(\s*([^)]+?)\s*(?:,|\))/gi;
  let match;
  while ((match = regex.exec(callText)) !== null) {
    const rawArg = String(match[1] || "").split(",")[0].trim();
    const quoted = rawArg.match(/^['"`]([^'"`]+)['"`]$/);
    if (quoted) {
      const table = normalizeIdentifier(quoted[1]);
      if (table) tables.push(table);
      continue;
    }
    const classLike = rawArg.match(/^([A-Za-z_][A-Za-z0-9_]*)$/);
    if (classLike) {
      tables.push(classToTable(classLike[1]));
    }
  }
  return [...new Set(tables.filter(Boolean))];
}

function buildPseudoSqlFromCallText(callText) {
  const methods = extractMethodChain(callText);
  if (!methods.some((method) => BUILDER_METHODS.has(method))) return null;

  const baseTable = inferBaseTable(callText);
  if (!baseTable) return null;

  const whereRegex =
    /(?:\.|->|::)(?:where|filter|filter_by|andwhere|orwhere|wherein|wheredate|wherebetween|eq|neq|lt|lte|gt|gte|match)\s*\(\s*([^)]+?)\s*(?:,|\))/gi;
  const orderRegex = /(?:\.|->|::)(?:order_by|orderby|order)\s*\(\s*([^)]+?)\s*(?:,|\))/gi;
  const groupRegex = /(?:\.|->|::)(?:group_by|groupby)\s*\(\s*([^)]+?)\s*(?:,|\))/gi;

  const whereCols = extractColumnsByPattern(callText, whereRegex, baseTable);
  const orderCols = extractColumnsByPattern(callText, orderRegex, baseTable);
  const groupCols = extractColumnsByPattern(callText, groupRegex, baseTable);
  const joinTables = extractJoinTables(callText);

  if (!whereCols.length && !orderCols.length && !groupCols.length && !joinTables.length) {
    return null;
  }

  const parts = [`SELECT * FROM ${baseTable}`];
  for (const joinTable of joinTables) {
    parts.push(`JOIN ${joinTable} ON 1 = 1`);
  }
  if (whereCols.length) {
    parts.push(`WHERE ${whereCols.map((col) => `${col} = ?`).join(" AND ")}`);
  }
  if (groupCols.length) {
    parts.push(`GROUP BY ${groupCols.join(", ")}`);
  }
  if (orderCols.length) {
    parts.push(`ORDER BY ${orderCols.join(", ")}`);
  }

  const pseudo = normalizeSql(parts.join(" "));
  return looksLikeSql(pseudo) ? pseudo : null;
}

function grammarNamesForLanguage(lang) {
  if (lang === "javascript") return ["tree-sitter-javascript.wasm", "javascript.wasm", "tree_sitter_javascript.wasm"];
  if (lang === "typescript") return ["tree-sitter-typescript.wasm", "typescript.wasm", "tree_sitter_typescript.wasm"];
  if (lang === "tsx") {
    return ["tree-sitter-tsx.wasm", "tsx.wasm", "tree_sitter_tsx.wasm", "tree-sitter-typescript.wasm"];
  }
  if (lang === "python") return ["tree-sitter-python.wasm", "python.wasm", "tree_sitter_python.wasm"];
  if (lang === "php") return ["tree-sitter-php.wasm", "php.wasm", "tree_sitter_php.wasm", "tree-sitter-php_only.wasm"];
  return [];
}

function grammarPackageDirsForLanguage(lang) {
  if (lang === "python") return ["tree-sitter-python"];
  if (lang === "javascript") return ["tree-sitter-javascript", "tree-sitter-typescript/node_modules/tree-sitter-javascript"];
  if (lang === "typescript" || lang === "tsx") return ["tree-sitter-typescript"];
  if (lang === "php") return ["tree-sitter-php"];
  return [];
}

function resolveGrammarPath(grammarDir, lang) {
  const searchDirs = [grammarDir];
  for (const pkgDir of grammarPackageDirsForLanguage(lang)) {
    searchDirs.push(path.join(grammarDir, pkgDir));
  }

  const candidates = grammarNamesForLanguage(lang);
  for (const searchDir of searchDirs) {
    for (const candidate of candidates) {
      const fullPath = path.join(searchDir, candidate);
      if (fs.existsSync(fullPath)) return fullPath;
    }
  }
  return null;
}

function isCallLikeNode(node) {
  const type = String(node?.type || "").toLowerCase();
  return /(call|invocation)/.test(type);
}

function addQuery(queries, seen, seenBySql, { sql, line, confidence, metadata }) {
  const normalized = normalizeSql(sql);
  if (!normalized || !looksLikeSql(normalized)) return;

  const priorLines = seenBySql.get(normalized) || [];
  if (priorLines.some((existingLine) => Math.abs(existingLine - line) <= 1)) return;

  const key = `${line}:${normalized}`;
  if (seen.has(key)) return;
  seen.add(key);
  priorLines.push(line);
  seenBySql.set(normalized, priorLines);
  queries.push({
    sql: normalized,
    line,
    confidence,
    metadata,
  });
}

function walk(node, source, queries, seen, seenBySql, lang) {
  if (isCallLikeNode(node)) {
    const callText = source.slice(node.startIndex, node.endIndex);
    const line = node.startPosition.row + 1;
    const methods = extractMethodChain(callText);

    const rawSql = extractRawSqlFromCallText(callText);
    if (rawSql) {
      addQuery(queries, seen, seenBySql, {
        sql: rawSql,
        line,
        confidence: 0.82,
        metadata: {
          extractor: "tree_sitter_wasm",
          language: lang,
          kind: "raw_call",
          node_type: node.type,
          methods,
        },
      });
    }

    const pseudoSql = buildPseudoSqlFromCallText(callText);
    if (pseudoSql) {
      addQuery(queries, seen, seenBySql, {
        sql: pseudoSql,
        line,
        confidence: 0.68,
        metadata: {
          extractor: "tree_sitter_wasm",
          language: lang,
          kind: "builder_chain",
          node_type: node.type,
          methods,
        },
      });
    }
  }

  for (let i = 0; i < node.namedChildCount; i += 1) {
    walk(node.namedChild(i), source, queries, seen, seenBySql, lang);
  }
}

async function main() {
  const args = parseArgs(process.argv.slice(2));
  const filePath = args.file;
  const lang = args.lang;
  const grammarDir = args["grammar-dir"];
  const warnings = [];

  if (!filePath || !lang || !grammarDir) {
    console.error(
      "Usage: node wasm_sql_extractor.mjs --file <path> --lang <python|javascript|typescript|tsx|php> --grammar-dir <path>"
    );
    process.exit(2);
  }

  const grammarCandidates = grammarNamesForLanguage(lang);
  if (!grammarCandidates.length) {
    console.error(`Unsupported language: ${lang}`);
    process.exit(2);
  }
  const grammarPath = resolveGrammarPath(grammarDir, lang);
  if (!grammarPath) {
    console.error(`Missing grammar file for '${lang}' in ${grammarDir}`);
    process.exit(2);
  }

  const loadedTreeSitter = await loadTreeSitterModule(grammarDir);
  const treeSitterModule = loadedTreeSitter.module;
  const runtimeDir = loadedTreeSitter.runtimeDir;
  const Language = treeSitterModule.Language ?? treeSitterModule.default?.Language;
  const Parser = treeSitterModule.Parser ?? treeSitterModule.default?.Parser;
  if (!Language || !Parser) {
    throw new Error("web-tree-sitter module loaded but Parser/Language exports are unavailable.");
  }

  const source = fs.readFileSync(filePath, "utf8");
  await Parser.init({
    locateFile(scriptName) {
      const exact = path.join(runtimeDir, scriptName);
      if (fs.existsSync(exact)) return pathToFileURL(exact).href;
      const fallback = path.join(runtimeDir, "tree-sitter.wasm");
      return pathToFileURL(fallback).href;
    },
  });
  const parser = new Parser();
  const language = await Language.load(grammarPath);
  parser.setLanguage(language);
  const tree = parser.parse(source);

  const queries = [];
  const seen = new Set();
  const seenBySql = new Map();
  walk(tree.rootNode, source, queries, seen, seenBySql, lang);

  process.stdout.write(JSON.stringify({ queries, warnings }));
}

main().catch((error) => {
  console.error(error?.message || String(error));
  process.exit(1);
});
