#!/usr/bin/env node
/**
 * Module boundary checker.
 *
 * Rules (see specs/F-001-scaffold/DESIGN.md):
 *  R1: Files inside <root>/modules/<mod>/ may only import their own module
 *      subtree, <root>/shared/, Node builtins, or npm packages.
 *  R2: Files outside modules may import a module only through its index.ts.
 *      Deep imports into <root>/modules/<mod>/... are violations.
 *
 * Usage: node scripts/check-module-boundaries.mjs <srcRoot>
 * Exit codes: 0 = clean, 1 = violations found, 2 = usage error.
 */

import { readdirSync, readFileSync, statSync } from 'node:fs';
import path from 'node:path';
import process from 'node:process';

function isBareSpecifier(specifier) {
  return !specifier.startsWith('.') && !specifier.startsWith('/');
}

function walk(dir, files = []) {
  for (const entry of readdirSync(dir)) {
    const full = path.join(dir, entry);
    const stat = statSync(full);
    if (stat.isDirectory()) {
      if (entry === 'node_modules' || entry === 'dist' || entry === 'fixtures') continue;
      walk(full, files);
    } else if (entry.endsWith('.ts') && !entry.endsWith('.d.ts')) {
      files.push(full);
    }
  }
  return files;
}

function extractSpecifiers(source) {
  const specifiers = [];
  const fromRegex = /(?:import|export)\s+[^'"]*?from\s*['"]([^'"]+)['"]/g;
  const sideEffectRegex = /^\s*import\s*['"]([^'"]+)['"]/gm;
  const dynamicRegex = /import\(\s*['"]([^'"]+)['"]\s*\)/g;
  let match;
  while ((match = fromRegex.exec(source)) !== null) specifiers.push(match[1]);
  while ((match = sideEffectRegex.exec(source)) !== null) specifiers.push(match[1]);
  while ((match = dynamicRegex.exec(source)) !== null) specifiers.push(match[1]);
  return specifiers;
}

/** Strip a .js/.ts extension so we can compare logical paths. */
function stripExtension(p) {
  return p.replace(/\.(js|ts|mjs|cjs)$/, '');
}

function checkFile(file, rootAbs, violations) {
  const source = readFileSync(file, 'utf8');
  const relFile = path.relative(rootAbs, file);
  const fileDir = path.dirname(file);
  const relFileParts = relFile.split(path.sep);
  const sourceInModule =
    relFileParts[0] === 'modules' && relFileParts.length >= 2 ? relFileParts[1] : null;

  for (const specifier of extractSpecifiers(source)) {
    if (isBareSpecifier(specifier)) continue; // npm package or node builtin

    const targetAbs = stripExtension(path.resolve(fileDir, specifier));
    const relTarget = path.relative(rootAbs, targetAbs);
    const targetParts = relTarget.split(path.sep);
    const targetInModule =
      targetParts[0] === 'modules' && targetParts.length >= 2 ? targetParts[1] : null;

    if (sourceInModule !== null) {
      // R1: stay inside own module or go to shared
      const ownModule = relTarget.startsWith(path.join('modules', sourceInModule) + path.sep);
      const toShared = targetParts[0] === 'shared';
      const toOtherModulePublicIndex =
        targetInModule !== null &&
        targetInModule !== sourceInModule &&
        targetParts.length === 3 &&
        targetParts[2] === 'index';
      if (!ownModule && !toShared && !toOtherModulePublicIndex) {
        violations.push(
          `R1 violation: ${relFile} imports "${specifier}" (escapes module "${sourceInModule}")`,
        );
      }
    } else if (targetInModule !== null) {
      // R2: outside code may only use a module's public index
      const isIndex = targetParts.length === 3 && targetParts[2] === 'index';
      if (!isIndex) {
        violations.push(
          `R2 violation: ${relFile} imports "${specifier}" (deep import into module "${targetInModule}", use its index)`,
        );
      }
    }
  }
}

async function main() {
  const rootArg = process.argv[2];
  if (!rootArg) {
    console.error('Usage: node scripts/check-module-boundaries.mjs <srcRoot>');
    process.exit(2);
  }
  const rootAbs = path.resolve(rootArg);
  const files = walk(rootAbs);
  const violations = [];
  for (const file of files) {
    checkFile(file, rootAbs, violations);
  }
  if (violations.length > 0) {
    for (const violation of violations) console.error(violation);
    console.error(`Boundary check FAILED: ${violations.length} violation(s)`);
    process.exit(1);
  }
  console.log(`Boundary check OK: ${files.length} file(s) checked`);
}

await main();
