import fs from 'fs';
import path from 'path';

const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
const SKIP_DIRS = new Set(['node_modules', '.git', '.next', 'dist', 'build', '.turbo', 'coverage', '__pycache__']);

const KIND_ARROW_FN = 0;
const KIND_FUNCTION = 1;
const KIND_CLASS = 7;
const KIND_METHOD = 9;

const FUNC_REGEX = /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)/;
const CLASS_REGEX = /^\s*(?:export\s+)?class\s+(\w+)/;
const ARROW_REGEX = /^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[^=]\w*)\s*=>/;
const FUNC_EXPR_REGEX = /^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?function\b/;
const METHOD_REGEX = /^\s*(?:static\s+)?(?:async\s+)?(?!constructor\b)(?!get\s+\w+\s*\(\))(?!set\s+\w+\s*\()(\w+)\s*\([^)]*\)\s*\{/;

const cache = new Map();

/**
 * @typedef {Object} SymbolEntry
 * @property {string} name
 * @property {number} kind
 * @property {string} filePath
 * @property {number} line
 * @property {string} snippet
 */

/**
 * @param {string} dirPath
 * @param {string} basePath
 * @returns {Promise<string[]>}
 */
async function findAllSourceFiles(dirPath, basePath) {
  const results = [];
  let items;
  try {
    items = await fs.promises.readdir(dirPath, { withFileTypes: true });
  } catch {
    return results;
  }
  for (const item of items) {
    if (item.name.startsWith('.') || SKIP_DIRS.has(item.name)) continue;
    const fullPath = path.join(dirPath, item.name);
    if (item.isFile()) {
      const ext = path.extname(item.name).toLowerCase();
      if (SOURCE_EXTENSIONS.has(ext)) {
        results.push(fullPath);
      }
    } else if (item.isDirectory()) {
      const children = await findAllSourceFiles(fullPath, basePath);
      results.push(...children);
    }
  }
  return results;
}

/**
 * @param {string[]} lines
 * @param {number} startLine
 * @returns {{ endLine: number, snippet: string }}
 */
function extractBracedBody(lines, startLine) {
  const startStr = lines[startLine];
  const braceIdx = startStr.indexOf('{');
  if (braceIdx === -1) {
    return { endLine: startLine, snippet: lines[startLine] };
  }

  let depth = 0;
  let endLine = startLine;

  for (let k = startLine; k < lines.length; k++) {
    const cl = lines[k];
    for (const ch of cl) {
      if (ch === '{') depth++;
      else if (ch === '}') {
        depth--;
        if (depth === 0) {
          endLine = k;
          break;
        }
      }
    }
    if (depth === 0 && k > startLine) break;
  }

  const indent = startStr.match(/^(\s*)/)?.[1] || '';
  const snippet = lines
    .slice(startLine, endLine + 1)
    .map((l) => l.startsWith(indent) ? l.slice(indent.length) : l)
    .join('\n');

  return { endLine, snippet };
}

/**
 * @param {string} filePath
 * @returns {Promise<SymbolEntry[]>}
 */
async function extractSymbolsFromFile(filePath) {
  const results = [];
  let content;
  try {
    content = await fs.promises.readFile(filePath, 'utf8');
  } catch {
    return results;
  }

  const lines = content.split('\n');

  const pushEntry = (name, kind, lineIdx, snippet) => {
    results.push({ name, kind, filePath, line: lineIdx + 1, snippet });
  };

  for (let i = 0; i < lines.length; i++) {
    const lineStr = lines[i];

    let m;
    if ((m = lineStr.match(FUNC_REGEX))) {
      const result = extractBracedBody(lines, i);
      pushEntry(m[1], KIND_FUNCTION, i, result.snippet);
      i = result.endLine;
      continue;
    }

    if ((m = lineStr.match(CLASS_REGEX))) {
      let classEndLine = i;
      const bodyResult = extractBracedBody(lines, i);
      classEndLine = bodyResult.endLine;
      pushEntry(m[1], KIND_CLASS, i, bodyResult.snippet);

      let depth = 0;
      let inClass = true;
      for (let j = i + 1; j <= classEndLine && inClass; j++) {
        const cl = lines[j];
        for (const ch of cl) {
          if (ch === '{') depth++;
          else if (ch === '}') {
            if (depth === 0) { inClass = false; break; }
            depth--;
          }
        }
        if (!inClass) break;
        const mm = cl.match(METHOD_REGEX);
        if (mm) {
          const methodResult = extractBracedBody(lines, j);
          pushEntry(mm[1], KIND_METHOD, j, methodResult.snippet);
          j = methodResult.endLine;
        }
      }
      i = classEndLine;
      continue;
    }

    if ((m = lineStr.match(ARROW_REGEX)) || (m = lineStr.match(FUNC_EXPR_REGEX))) {
      for (let k = i; k < Math.min(i + 10, lines.length); k++) {
        const cl = lines[k];
        if (cl.includes('=>') || (cl.includes('function') && cl.includes('{'))) {
          const { endLine, snippet } = extractBracedBody(lines, cl.includes('{') ? k : (k < lines.length && lines[k].includes('{') ? k : i));
          pushEntry(m[1], KIND_ARROW_FN, i, snippet);
          i = endLine;
          break;
        }
        if (cl.includes(';') || (cl.trim().endsWith(')') && !cl.includes('{') && !cl.includes('=>'))) {
          const end = k;
          pushEntry(m[1], KIND_ARROW_FN, i, lines.slice(i, end + 1).join('\n'));
          i = end;
          break;
        }
      }
      continue;
    }
  }

  return results;
}

/**
 * @param {string} dirPath
 * @returns {Promise<Map<string, SymbolEntry[]>>}
 */
async function getOrBuildIndex(dirPath) {
  const resolved = path.resolve(dirPath);
  let mtimeSum = 0;
  const files = await findAllSourceFiles(resolved, resolved);

  for (const fp of files) {
    try {
      const stat = await fs.promises.stat(fp);
      mtimeSum += stat.mtimeMs;
    } catch {}
  }

  const fileSet = files.sort().join('|');
  const cacheKey = `${resolved}::${fileSet}::${mtimeSum}`;

  if (cache.has(cacheKey)) {
    return cache.get(cacheKey);
  }

  const index = new Map();
  const promises = files.map(async (filePath) => {
    const symbols = await extractSymbolsFromFile(filePath);
    if (symbols.length > 0) {
      index.set(filePath, symbols);
    }
  });
  await Promise.all(promises);

  if (cache.size > 20) {
    const firstKey = cache.keys().next().value;
    cache.delete(firstKey);
  }
  cache.set(cacheKey, index);

  return index;
}

/**
 * @param {string} query
 * @param {Object} [options]
 * @param {string} [options.directory]
 * @returns {Promise<SymbolEntry[]>}
 */
export async function findSymbols(query, options = {}) {
  if (!options.directory) return [];
  const q = query.trim().toLowerCase();
  if (!q) return [];

  const index = await getOrBuildIndex(options.directory);

  const exact = [];
  const partial = [];

  for (const [, symbols] of index) {
    for (const sym of symbols) {
      const nameLower = sym.name.toLowerCase();
      if (nameLower === q) {
        exact.push(sym);
      } else if (nameLower.includes(q)) {
        partial.push(sym);
      }
    }
  }

  exact.sort((a, b) => a.name.localeCompare(b.name));
  partial.sort((a, b) => a.name.localeCompare(b.name));

  return [...exact, ...partial];
}
