#!/usr/bin/env node

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

const ROOT_DIR = path.resolve(__dirname, '..');
const SRC_DIR = path.join(ROOT_DIR, 'src');
const REPORT_DIR = path.join(ROOT_DIR, 'docs', 'reports');
const JSON_REPORT_PATH = path.join(REPORT_DIR, 'hardening-inventory.json');
const MD_REPORT_PATH = path.join(REPORT_DIR, 'hardening-inventory.md');

const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
const HOTPATH_PATTERNS = [
  /^src\/web-server\//,
  /^src\/commands\//,
  /^src\/cliproxy\//,
  /^src\/management\//,
  /^src\/auth\//,
  /^src\/delegation\//,
  /^src\/utils\//,
  /^src\/ccs\.ts$/,
];

const SYNC_CALL_NAMES = [
  'accessSync',
  'appendFileSync',
  'chmodSync',
  'chownSync',
  'closeSync',
  'copyFileSync',
  'cpSync',
  'existsSync',
  'fstatSync',
  'fsyncSync',
  'ftruncateSync',
  'futimesSync',
  'lchmodSync',
  'lchownSync',
  'linkSync',
  'lstatSync',
  'mkdirSync',
  'mkdtempSync',
  'openSync',
  'opendirSync',
  'readFileSync',
  'readdirSync',
  'readlinkSync',
  'readSync',
  'readvSync',
  'realpathSync',
  'renameSync',
  'rmSync',
  'rmdirSync',
  'statSync',
  'symlinkSync',
  'truncateSync',
  'unlinkSync',
  'utimesSync',
  'writeFileSync',
  'writeSync',
  'writevSync',
];
const SYNC_CALL_CAPTURE_REGEX = new RegExp(
  `(?:\\bfs(?:\\s*\\?\\.)?\\s*\\.\\s*|(?<![\\w$.]))(${SYNC_CALL_NAMES.join('|')})\\s*\\(`,
  'g'
);
const LEGACY_MARKER_REGEX =
  /(?:\blegacy\b|\bshim\b|backward compatibility|backwards compatibility|compatibility layer|deprecated.*re-export|re-export.*compatibility)/i;
const REGEX_LITERAL_KEYWORDS = new Set([
  'return',
  'throw',
  'case',
  'else',
  'do',
  'delete',
  'void',
  'typeof',
  'instanceof',
  'in',
  'of',
  'yield',
  'await',
  'new',
]);

function toPosixPath(filePath) {
  return filePath.split(path.sep).join('/');
}

function relativePath(filePath) {
  return toPosixPath(path.relative(ROOT_DIR, filePath));
}

function isSourceFile(filePath) {
  return SOURCE_EXTENSIONS.has(path.extname(filePath));
}

function isHotpath(filePath) {
  return HOTPATH_PATTERNS.some((pattern) => pattern.test(filePath));
}

function walkFiles(dirPath) {
  const output = [];
  const entries = fs.readdirSync(dirPath, { withFileTypes: true });

  for (const entry of entries) {
    const fullPath = path.join(dirPath, entry.name);
    if (entry.isDirectory()) {
      output.push(...walkFiles(fullPath));
      continue;
    }

    if (entry.isFile() && isSourceFile(fullPath)) {
      output.push(fullPath);
    }
  }

  return output;
}

function uniqueSorted(values) {
  return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
}

function sortByCountDesc(items) {
  return [...items].sort((a, b) => {
    if (b.count !== a.count) return b.count - a.count;
    return a.file.localeCompare(b.file);
  });
}

function summarize(items, limit = 10) {
  return sortByCountDesc(items)
    .slice(0, limit)
    .map((item) => ({
      file: item.file,
      count: item.count,
      calls: uniqueSorted(item.calls || []),
      markers: uniqueSorted(item.markers || []),
    }));
}

function isRegexLiteralStart(previousSignificantChar, previousIdentifier) {
  return (
    previousSignificantChar === '' ||
    '([{:;,=!?+-*%^&|~<>'.includes(previousSignificantChar) ||
    REGEX_LITERAL_KEYWORDS.has(previousIdentifier)
  );
}

function stripComments(sourceText) {
  let output = '';
  let index = 0;
  let inLineComment = false;
  let inBlockComment = false;
  let inSingleQuote = false;
  let inDoubleQuote = false;
  let inTemplateLiteral = false;
  let inRegexLiteral = false;
  let inRegexCharClass = false;
  let previousSignificantChar = '';
  let previousIdentifier = '';

  while (index < sourceText.length) {
    const current = sourceText[index];
    const next = sourceText[index + 1];

    if (inLineComment) {
      if (current === '\n' || current === '\r') {
        inLineComment = false;
        output += current;
      } else {
        output += ' ';
      }
      index += 1;
      continue;
    }

    if (inBlockComment) {
      if (current === '*' && next === '/') {
        output += '  ';
        index += 2;
        inBlockComment = false;
        continue;
      }

      output += current === '\n' || current === '\r' ? current : ' ';
      index += 1;
      continue;
    }

    if (inRegexLiteral) {
      if (current === '\n' || current === '\r') {
        output += current;
        index += 1;
        inRegexLiteral = false;
        inRegexCharClass = false;
        continue;
      }

      output += ' ';

      if (current === '\\') {
        output += next === '\n' || next === '\r' ? next : ' ';
        index += 2;
        continue;
      }

      if (!inRegexCharClass && current === '[') {
        inRegexCharClass = true;
        index += 1;
        continue;
      }

      if (inRegexCharClass && current === ']') {
        inRegexCharClass = false;
        index += 1;
        continue;
      }

      if (!inRegexCharClass && current === '/') {
        index += 1;
        while (index < sourceText.length && /[a-z]/i.test(sourceText[index])) {
          output += ' ';
          index += 1;
        }
        inRegexLiteral = false;
        previousSignificantChar = 'r';
        previousIdentifier = '';
        continue;
      }

      index += 1;
      continue;
    }

    if (inSingleQuote) {
      output += current === '\n' || current === '\r' ? current : ' ';
      if (current === '\\') {
        output += next === '\n' || next === '\r' ? next : ' ';
        index += 2;
        continue;
      }
      if (current === "'") {
        inSingleQuote = false;
        previousSignificantChar = 's';
        previousIdentifier = '';
      }
      index += 1;
      continue;
    }

    if (inDoubleQuote) {
      output += current === '\n' || current === '\r' ? current : ' ';
      if (current === '\\') {
        output += next === '\n' || next === '\r' ? next : ' ';
        index += 2;
        continue;
      }
      if (current === '"') {
        inDoubleQuote = false;
        previousSignificantChar = 's';
        previousIdentifier = '';
      }
      index += 1;
      continue;
    }

    if (inTemplateLiteral) {
      output += current === '\n' || current === '\r' ? current : ' ';
      if (current === '\\') {
        output += next === '\n' || next === '\r' ? next : ' ';
        index += 2;
        continue;
      }
      if (current === '`') {
        inTemplateLiteral = false;
        previousSignificantChar = 's';
        previousIdentifier = '';
      }
      index += 1;
      continue;
    }

    if (current === '/' && next === '/') {
      output += '  ';
      index += 2;
      inLineComment = true;
      continue;
    }

    if (current === '/' && next === '*') {
      output += '  ';
      index += 2;
      inBlockComment = true;
      continue;
    }

    if (current === '/' && isRegexLiteralStart(previousSignificantChar, previousIdentifier)) {
      output += ' ';
      index += 1;
      inRegexLiteral = true;
      inRegexCharClass = false;
      continue;
    }

    if (/[A-Za-z_$]/.test(current)) {
      let tokenEnd = index + 1;
      while (tokenEnd < sourceText.length && /[A-Za-z0-9_$]/.test(sourceText[tokenEnd])) {
        tokenEnd += 1;
      }
      const token = sourceText.slice(index, tokenEnd);
      output += token;
      previousSignificantChar = 'i';
      previousIdentifier = token;
      index = tokenEnd;
      continue;
    }

    if (current === "'") {
      inSingleQuote = true;
      output += ' ';
      index += 1;
      continue;
    }

    if (current === '"') {
      inDoubleQuote = true;
      output += ' ';
      index += 1;
      continue;
    }

    if (current === '`') {
      inTemplateLiteral = true;
      output += ' ';
      index += 1;
      continue;
    }

    output += current;
    if (!/\s/.test(current)) {
      previousSignificantChar = current;
      previousIdentifier = '';
    }
    index += 1;
  }

  return output;
}

function collectSyncCallSites(sourceText) {
  const sanitizedSource = stripComments(sourceText);
  const captureRegex = new RegExp(SYNC_CALL_CAPTURE_REGEX.source, SYNC_CALL_CAPTURE_REGEX.flags);
  const calls = [];

  for (const match of sanitizedSource.matchAll(captureRegex)) {
    calls.push(match[1]);
  }

  return {
    count: calls.length,
    calls,
  };
}

function buildReport() {
  const files = walkFiles(SRC_DIR);
  const syncEntries = [];
  const legacyEntries = [];

  for (const fullPath of files) {
    const file = relativePath(fullPath);
    const sourceText = fs.readFileSync(fullPath, 'utf8');
    const lines = sourceText.split(/\r?\n/);
    const { count: syncCount, calls: syncCalls } = collectSyncCallSites(sourceText);
    let legacyCount = 0;
    const legacyMarkers = [];

    for (const line of lines) {
      if (LEGACY_MARKER_REGEX.test(line)) {
        legacyCount += 1;
        const normalized = line.trim();
        if (normalized.length > 0) {
          legacyMarkers.push(normalized);
        }
      }
    }

    if (syncCount > 0) {
      syncEntries.push({
        file,
        count: syncCount,
        calls: syncCalls,
        hotpath: isHotpath(file),
      });
    }

    if (legacyCount > 0) {
      legacyEntries.push({
        file,
        count: legacyCount,
        markers: legacyMarkers,
      });
    }
  }

  const syncHotpathEntries = syncEntries.filter((entry) => entry.hotpath);
  const totalSyncCount = syncEntries.reduce((acc, entry) => acc + entry.count, 0);
  const totalSyncHotpathCount = syncHotpathEntries.reduce((acc, entry) => acc + entry.count, 0);
  const totalLegacyMarkers = legacyEntries.reduce((acc, entry) => acc + entry.count, 0);

  return {
    scope: 'src/**/*.{ts,tsx,js,jsx,mjs,cjs}',
    syncFs: {
      totalOccurrences: totalSyncCount,
      filesAffected: syncEntries.length,
      hotpathOccurrences: totalSyncHotpathCount,
      hotpathFilesAffected: syncHotpathEntries.length,
      topHotpathFiles: summarize(syncHotpathEntries),
      topFilesOverall: summarize(syncEntries),
    },
    legacyShim: {
      totalMarkers: totalLegacyMarkers,
      filesAffected: legacyEntries.length,
      topFiles: summarize(legacyEntries),
      explicitShimFiles: uniqueSorted(
        legacyEntries
          .map((entry) => entry.file)
          .filter((file) => /shim|re-export|compat/i.test(path.basename(file)))
      ),
    },
  };
}

function renderMarkdown(report) {
  const lines = [];

  lines.push('# Hardening Inventory Report');
  lines.push('');
  lines.push(`Scope: \`${report.scope}\``);
  lines.push('');
  lines.push('## Summary');
  lines.push('');
  lines.push('| Metric | Value |');
  lines.push('|---|---:|');
  lines.push(`| Sync fs occurrences (all) | ${report.syncFs.totalOccurrences} |`);
  lines.push(`| Sync fs files affected (all) | ${report.syncFs.filesAffected} |`);
  lines.push(`| Sync fs occurrences (runtime hotpaths) | ${report.syncFs.hotpathOccurrences} |`);
  lines.push(`| Sync fs files affected (runtime hotpaths) | ${report.syncFs.hotpathFilesAffected} |`);
  lines.push(`| Legacy shim markers | ${report.legacyShim.totalMarkers} |`);
  lines.push(`| Legacy shim files affected | ${report.legacyShim.filesAffected} |`);
  lines.push('');

  lines.push('## Top Runtime Hotpath Sync fs Files');
  lines.push('');
  lines.push('| File | Sync Calls | API Names |');
  lines.push('|---|---:|---|');

  for (const item of report.syncFs.topHotpathFiles) {
    lines.push(`| \`${item.file}\` | ${item.count} | ${item.calls.join(', ')} |`);
  }

  if (report.syncFs.topHotpathFiles.length === 0) {
    lines.push('| _none_ | 0 | - |');
  }

  lines.push('');
  lines.push('## Top Legacy Shim Marker Files');
  lines.push('');
  lines.push('| File | Marker Count |');
  lines.push('|---|---:|');

  for (const item of report.legacyShim.topFiles) {
    lines.push(`| \`${item.file}\` | ${item.count} |`);
  }

  if (report.legacyShim.topFiles.length === 0) {
    lines.push('| _none_ | 0 |');
  }

  lines.push('');
  lines.push('## Explicit Shim/Re-export Files');
  lines.push('');
  for (const file of report.legacyShim.explicitShimFiles) {
    lines.push(`- \`${file}\``);
  }
  if (report.legacyShim.explicitShimFiles.length === 0) {
    lines.push('- _none_');
  }

  lines.push('');
  return lines.join('\n');
}

function main() {
  const report = buildReport();

  fs.mkdirSync(REPORT_DIR, { recursive: true });
  fs.writeFileSync(JSON_REPORT_PATH, JSON.stringify(report, null, 2) + '\n', 'utf8');
  fs.writeFileSync(MD_REPORT_PATH, renderMarkdown(report), 'utf8');

  const relJson = relativePath(JSON_REPORT_PATH);
  const relMd = relativePath(MD_REPORT_PATH);

  console.log(`[hardening-inventory] generatedAt=${new Date().toISOString()}`);
  console.log(
    `[hardening-inventory] sync-fs total=${report.syncFs.totalOccurrences}, hotpath=${report.syncFs.hotpathOccurrences}`
  );
  console.log(
    `[hardening-inventory] legacy markers total=${report.legacyShim.totalMarkers}, files=${report.legacyShim.filesAffected}`
  );
  console.log(`[hardening-inventory] wrote ${relJson}`);
  console.log(`[hardening-inventory] wrote ${relMd}`);
}

if (require.main === module) {
  main();
}

module.exports = {
  buildReport,
  collectSyncCallSites,
  renderMarkdown,
  stripComments,
};
