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

export interface CheckError {
  type:
    | "missing-doc"
    | "orphaned-doc"
    | "missing-test-case"
    | "extra-test-case"
    | "missing-test-file"
    | "orphaned-spec"
    | "missing-actor-doc"
    | "missing-actor-fixture"
    | "missing-page-object"
    | "missing-spec-fixture";
  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));
  }

  // 1:1 file-based sync check (command, query, resolver)
  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;
  }

  // Model sync check: db files ↔ ### Models section entries (many db files → one doc)
  if (config.modulesRoot) {
    const model = moduleModelCategory(config.modulesRoot);
    const sources = await fg(model.sourcePattern, { cwd });
    const docs = await fg(model.docPattern, { cwd });

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

    for (const sourcePath of sources) {
      const fileName = path.basename(sourcePath);
      if (shouldExclude(fileName, model.exclusions)) continue;
      const basename = path.basename(sourcePath, path.extname(sourcePath));
      sourceBasenames.set(basename.toLowerCase(), sourcePath);
    }

    for (const docPath of docs) {
      const content = await fs.readFile(path.join(cwd, docPath), "utf-8");
      for (const name of parseModelsSection(content)) {
        docModelNames.set(name.toLowerCase(), docPath);
      }
    }

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

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

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

  return {
    exitCode: errors.length > 0 ? 1 : 0,
    errors,
    summary: {
      categoriesChecked: allCategories.length + (config.modulesRoot ? 1 : 0),
      totalSources,
      totalDocs,
    },
  };
}

/**
 * Parse the `### Models` section from a markdown doc and return the listed
 * model names.  Each line matching `- ModelName` within the section is
 * collected until the next heading or end of file.
 */
export function parseModelsSection(content: string): string[] {
  const lines = content.split("\n");
  let inSection = false;
  const names: string[] = [];
  for (const line of lines) {
    if (/^### Models\s*$/.test(line)) {
      inSection = true;
      continue;
    }
    if (inSection) {
      if (/^#{1,3}\s/.test(line)) break;
      const m = /^-\s+(\S+)/.exec(line);
      if (m) names.push(m[1]);
    }
  }
  return names;
}
