/**
 * RUL Scanner — Rules Validator
 * Validates .claude/rules/ files: glob matching against real files, orphan detection, frontmatter.
 * Finding IDs: CA-RUL-NNN
 */

import { readTextFile } from './lib/file-discovery.mjs';
import { finding, scannerResult } from './lib/output.mjs';
import { SEVERITY } from './lib/severity.mjs';
import { parseFrontmatter } from './lib/yaml-parser.mjs';
import { lineCount, truncate } from './lib/string-utils.mjs';
import { readdir, stat } from 'node:fs/promises';
import { join, resolve, relative } from 'node:path';

const SCANNER = 'RUL';

/**
 * Scan .claude/rules/ directories for issues.
 * @param {string} targetPath
 * @param {{ files: import('./lib/file-discovery.mjs').ConfigFile[] }} discovery
 * @returns {Promise<object>}
 */
export async function scan(targetPath, discovery) {
  const start = Date.now();
  const ruleFiles = discovery.files.filter(f => f.type === 'rule');
  const findings = [];
  let filesScanned = 0;

  if (ruleFiles.length === 0) {
    return scannerResult(SCANNER, 'skipped', [], 0, Date.now() - start);
  }

  // Collect all real files in the project for glob matching
  const projectFiles = await collectProjectFiles(targetPath);

  for (const file of ruleFiles) {
    const content = await readTextFile(file.absPath);
    if (!content) continue;
    filesScanned++;

    const { frontmatter, body, bodyStartLine } = parseFrontmatter(content);
    const lines = lineCount(content);

    // --- Frontmatter checks ---
    if (!frontmatter) {
      // Rules without frontmatter are "always on" — not necessarily wrong, just note it
      if (lines > 5) {
        findings.push(finding({
          scanner: SCANNER,
          severity: SEVERITY.info,
          title: 'Rule has no frontmatter (always active)',
          description: `${file.relPath} has no YAML frontmatter. It will be loaded for ALL files. Add paths: frontmatter to scope it.`,
          file: file.absPath,
          recommendation: 'Add frontmatter with paths: to limit when this rule applies.',
        }));
      }
    } else {
      // Check for paths/globs frontmatter
      const paths = frontmatter.paths || frontmatter.globs;

      if (frontmatter.globs && !frontmatter.paths) {
        findings.push(finding({
          scanner: SCANNER,
          severity: SEVERITY.low,
          title: 'Rule uses deprecated "globs" field',
          description: `${file.relPath} uses "globs:" which is legacy. Use "paths:" instead.`,
          file: file.absPath,
          evidence: `globs: ${JSON.stringify(frontmatter.globs)}`,
          recommendation: 'Rename "globs:" to "paths:" in frontmatter.',
          autoFixable: true,
        }));
      }

      if (paths) {
        const patterns = Array.isArray(paths) ? paths : [paths];

        for (const pattern of patterns) {
          if (typeof pattern !== 'string') continue;

          // Check if pattern matches any real files
          const matchCount = countGlobMatches(pattern, projectFiles, targetPath);
          if (matchCount === 0) {
            findings.push(finding({
              scanner: SCANNER,
              severity: SEVERITY.high,
              title: 'Rule path pattern matches no files',
              description: `${file.relPath}: pattern "${pattern}" matches 0 files. This rule will never activate.`,
              file: file.absPath,
              evidence: `paths: "${pattern}"`,
              recommendation: 'Check the glob pattern. Common issues: wrong directory name, missing **, incorrect extension.',
              autoFixable: false,
            }));
          }
        }
      }
    }

    // --- Content quality checks ---
    if (lines < 2) {
      findings.push(finding({
        scanner: SCANNER,
        severity: SEVERITY.low,
        title: 'Rule file is nearly empty',
        description: `${file.relPath} has only ${lines} line(s).`,
        file: file.absPath,
        recommendation: 'Add meaningful content or remove the file.',
        autoFixable: false,
      }));
    }

    // Check for overly broad rules (huge files without path scoping)
    if (!frontmatter?.paths && !frontmatter?.globs && lines > 50) {
      findings.push(finding({
        scanner: SCANNER,
        severity: SEVERITY.medium,
        title: 'Large unscoped rule file',
        description: `${file.relPath} has ${lines} lines and no path scoping. It loads into context for every file interaction.`,
        file: file.absPath,
        evidence: `${lines} lines, no paths: frontmatter`,
        recommendation: 'Add paths: frontmatter to scope this rule, or split into smaller path-specific rules.',
        autoFixable: false,
      }));
    }

    // Check file extension
    if (!file.absPath.endsWith('.md')) {
      findings.push(finding({
        scanner: SCANNER,
        severity: SEVERITY.medium,
        title: 'Rule file is not .md',
        description: `${file.relPath} is not a .md file. Only .md files are loaded from rules/.`,
        file: file.absPath,
        recommendation: 'Rename to .md extension.',
        autoFixable: true,
      }));
    }
  }

  return scannerResult(SCANNER, 'ok', findings, filesScanned, Date.now() - start);
}

/**
 * Collect project file paths for glob matching (limited depth).
 * @param {string} targetPath
 * @returns {Promise<string[]>}
 */
async function collectProjectFiles(targetPath, depth = 0) {
  if (depth > 4) return [];
  const SKIP = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.nuxt', 'vendor']);
  const files = [];

  let entries;
  try {
    entries = await readdir(targetPath, { withFileTypes: true });
  } catch {
    return files;
  }

  for (const entry of entries) {
    const fullPath = join(targetPath, entry.name);
    if (entry.isFile()) {
      files.push(fullPath);
    } else if (entry.isDirectory() && !SKIP.has(entry.name) && !entry.name.startsWith('.')) {
      const subFiles = await collectProjectFiles(fullPath, depth + 1);
      files.push(...subFiles);
      if (files.length > 5000) break; // Safety limit
    }
  }

  return files;
}

/**
 * Count how many files match a simplified glob pattern.
 * Supports: *, **, specific extensions.
 * @param {string} pattern
 * @param {string[]} files
 * @param {string} basePath
 * @returns {number}
 */
function countGlobMatches(pattern, files, basePath) {
  try {
    const regex = globToRegex(pattern);
    let count = 0;
    for (const file of files) {
      const rel = relative(basePath, file);
      if (regex.test(rel)) count++;
    }
    return count;
  } catch {
    return -1; // Pattern parsing error — don't report as orphan
  }
}

/**
 * Convert a simple glob pattern to a regex.
 * Handles ** matching zero or more path segments.
 * @param {string} pattern
 * @returns {RegExp}
 */
function globToRegex(pattern) {
  let regex = pattern
    .replace(/\./g, '\\.')
    .replace(/\/\*\*\//g, '{{GLOBSTAR_SLASH}}')
    .replace(/\*\*/g, '{{GLOBSTAR}}')
    .replace(/\*/g, '[^/]*')
    .replace(/\{\{GLOBSTAR_SLASH\}\}/g, '(?:/.+/|/)')  // **/ matches 0+ intermediate dirs
    .replace(/\{\{GLOBSTAR\}\}/g, '.*')
    .replace(/\?/g, '[^/]');

  // Handle leading patterns
  if (!regex.startsWith('.*') && !regex.startsWith('/')) {
    regex = '(?:^|/)' + regex;
  }

  return new RegExp(regex);
}
