import path from "node:path";
import fg from "fast-glob";
import { moduleCategories, appComposeCategories, type CategoryConfig } from "./discovery";

export interface CheckError {
  type:
    | "missing-doc"
    | "orphaned-doc"
    | "missing-test-case"
    | "extra-test-case"
    | "missing-test-file";
  category: string;
  sourcePath?: string;
  docPath?: string;
  expectedBasename: string;
}

export interface SyncCheckResult {
  exitCode: number;
  errors: CheckError[];
  summary: {
    categoriesChecked: number;
    totalSources: number;
    totalDocs: number;
  };
}

function shouldExclude(fileName: string, exclusions: RegExp[]): boolean {
  return exclusions.some((pattern) => pattern.test(fileName));
}

export async function runSourceSyncCheck(
  config: { modulesRoot?: string; appRoot?: string },
  cwd: string,
): Promise<SyncCheckResult> {
  const errors: CheckError[] = [];
  let totalSources = 0;
  let totalDocs = 0;

  const allCategories: CategoryConfig[] = [];
  if (config.modulesRoot) {
    allCategories.push(...moduleCategories(config.modulesRoot));
  }
  if (config.appRoot) {
    allCategories.push(...appComposeCategories(config.appRoot));
  }

  for (const category of allCategories) {
    const sources = await fg(category.sourcePattern, { cwd });
    const docs = await fg(category.docPattern, { cwd });

    const sourceBasenames = new Map<string, string>();
    const docBasenames = new Map<string, string>();

    for (const sourcePath of sources) {
      const fileName = path.basename(sourcePath);
      if (shouldExclude(fileName, category.exclusions)) continue;
      let basename = path.basename(sourcePath, path.extname(sourcePath));
      if (basename.endsWith(".generated")) {
        basename = basename.slice(0, -".generated".length);
      }
      sourceBasenames.set(basename.toLowerCase(), sourcePath);
    }

    for (const docPath of docs) {
      const basename = path.basename(docPath, path.extname(docPath));
      docBasenames.set(basename.toLowerCase(), docPath);
    }

    for (const [basename, sourcePath] of sourceBasenames) {
      if (!docBasenames.has(basename)) {
        errors.push({
          type: "missing-doc",
          category: category.name,
          sourcePath,
          expectedBasename: basename,
        });
      }
    }

    for (const [basename, docPath] of docBasenames) {
      if (!sourceBasenames.has(basename)) {
        errors.push({
          type: "orphaned-doc",
          category: category.name,
          docPath,
          expectedBasename: basename,
        });
      }
    }

    totalSources += sourceBasenames.size;
    totalDocs += docBasenames.size;
  }

  return {
    exitCode: errors.length > 0 ? 1 : 0,
    errors,
    summary: {
      categoriesChecked: allCategories.length,
      totalSources,
      totalDocs,
    },
  };
}
