const fs = require('node:fs');
const path = require('node:path');

const DEFAULT_INCLUDE = ['.tsx', '.ts'];
const DEFAULT_EXCLUDE_PATTERNS = ['.test.', '.spec.', '.stories.', 'node_modules'];

function scanUiSelectors(options) {
  const uiPath = path.resolve(options.uiPath);
  const language = options.language ?? 'react-tsx';
  if (language !== 'react-tsx') {
    throw new Error(`Unsupported uiScanner.language "${language}". Supported values: react-tsx`);
  }

  if (!fs.existsSync(uiPath)) {
    throw new Error(`UI path "${uiPath}" does not exist.`);
  }

  const include = normalizeExtensions(options.include, DEFAULT_INCLUDE);
  const excludePatterns = options.exclude?.length ? options.exclude : DEFAULT_EXCLUDE_PATTERNS;
  const files = walkFiles(uiPath, include, excludePatterns);
  const selectors = {};
  const grids = {};
  const dynamic = [];
  const selectorFiles = {};

  for (const filePath of files) {
    const source = fs.readFileSync(filePath, 'utf8');
    const outputFile = resolveSelectorOutputFile(filePath, uiPath, options.selectorOutput);
    collectSelectorsFromSource(source, filePath, selectors, grids, dynamic, selectorFiles, outputFile);
  }

  return {
    language,
    scannedFileCount: files.length,
    selectors,
    grids,
    dynamic,
    selectorFiles: Object.keys(selectorFiles).length > 0 ? selectorFiles : undefined
  };
}

function mergeIntoConfig(existingConfig, scanResult) {
  const next = { ...existingConfig };
  if (!scanResult.selectorFiles) {
    next.selectors = {
      ...(existingConfig.selectors ?? {}),
      ...scanResult.selectors
    };
  }
  next.grids = {
    ...(existingConfig.grids ?? {}),
    ...scanResult.grids
  };
  next.uiScanner = {
    ...(existingConfig.uiScanner ?? {}),
    language: scanResult.language,
    include: existingConfig.uiScanner?.include ?? DEFAULT_INCLUDE,
    exclude: existingConfig.uiScanner?.exclude ?? DEFAULT_EXCLUDE_PATTERNS
  };
  return next;
}

function collectSelectorsFromSource(source, filePath, selectors, grids, dynamic, selectorFiles, outputFile) {
  const dynamicRegex = /data-test-id\s*=\s*(?:\{\s*)?`([^`]*\$\{[^`]*)`(?:\s*\})?|data-testid\s*=\s*(?:\{\s*)?`([^`]*\$\{[^`]*)`(?:\s*\})?/g;
  let dynamicMatch;
  while ((dynamicMatch = dynamicRegex.exec(source)) !== null) {
    dynamic.push({
      filePath,
      expression: dynamicMatch[0]
    });
  }

  const staticRegex = /(data-test-id|data-testid)\s*=\s*(?:\{\s*)?["'`]([^"'`$]+)["'`](?:\s*\})?/g;
  let match;
  while ((match = staticRegex.exec(source)) !== null) {
    const attributeName = match[1];
    const testId = match[2];
    if (!testId || testId.includes('${')) {
      continue;
    }

    const alias = normalizeAlias(testId);
    const selector = `[${attributeName}="${escapeAttribute(testId)}"]`;
    selectors[alias] = selector;
    if (outputFile) {
      const target = selectorFiles[outputFile] ?? { selectors: {}, grids: {} };
      target.selectors[alias] = selector;
      selectorFiles[outputFile] = target;
    }

    if (isGridLike(testId, filePath)) {
      grids[alias] = selector;
      if (outputFile) {
        selectorFiles[outputFile].grids[alias] = selector;
      }
    }
  }
}

function walkFiles(rootPath, includeExtensions, excludePatterns) {
  const results = [];
  const queue = [rootPath];

  while (queue.length > 0) {
    const current = queue.shift();
    const stats = fs.statSync(current);
    if (stats.isDirectory()) {
      for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
        queue.push(path.join(current, entry.name));
      }
      continue;
    }

    if (!includeExtensions.includes(path.extname(current))) {
      continue;
    }

    const normalizedCurrent = current.split(path.sep).join('/');
    if (excludePatterns.some((pattern) => normalizedCurrent.includes(String(pattern).split(path.sep).join('/')))) {
      continue;
    }

    results.push(current);
  }

  return results.sort();
}

function normalizeExtensions(include, fallback) {
  const values = include?.length ? include : fallback;
  return values.map((value) => (value.startsWith('.') ? value : `.${value}`));
}

function normalizeAlias(testId) {
  return testId.trim();
}

function isGridLike(testId, filePath) {
  const normalized = testId.toLowerCase();
  return normalized.includes('grid') || normalized.endsWith('-list') || filePath.toLowerCase().includes('grid');
}

function escapeAttribute(value) {
  return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}

function resolveSelectorOutputFile(filePath, uiPath, selectorOutput) {
  if (selectorOutput?.mode !== 'split') {
    return undefined;
  }

  const strategy = selectorOutput.strategy ?? 'by-folder';
  const normalizedRelative = path.relative(uiPath, filePath).split(path.sep).join('/');
  const strippedRelative = stripConfiguredPrefixes(normalizedRelative, selectorOutput.stripPathPrefixes ?? []);

  if (strategy === 'single-file') {
    return 'generated.json';
  }

  if (strategy === 'by-file') {
    return strippedRelative.replace(/\.[^/.]+$/, '.json');
  }

  const folder = path.posix.dirname(strippedRelative);
  return `${folder === '.' ? 'root' : folder}.json`;
}

function stripConfiguredPrefixes(relativePath, prefixes) {
  for (const prefix of prefixes) {
    const normalizedPrefix = String(prefix).replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
    if (!normalizedPrefix) {
      continue;
    }
    if (relativePath === normalizedPrefix) {
      return '';
    }
    if (relativePath.startsWith(`${normalizedPrefix}/`)) {
      return relativePath.slice(normalizedPrefix.length + 1);
    }
  }

  return relativePath;
}

module.exports = {
  DEFAULT_EXCLUDE_PATTERNS,
  DEFAULT_INCLUDE,
  mergeIntoConfig,
  scanUiSelectors
};
