import fs from "node:fs/promises";
import path from "node:path";
import fg from "fast-glob";
import { type CommandResult, success } from "../../../lib/command-result";
import { classifyViolationBucket, findNamingViolations } from "./naming-rules";

export type StructureViolationType = "missing-required-dir" | "naming-violation";

export interface StructureViolation {
  type: StructureViolationType;
  path: string;
  rule?: string;
}

export interface ModulesMeasure {
  count: number;
  violations: StructureViolation[];
}

export interface AppStructureMeasure {
  count: number;
  violations: StructureViolation[];
}

export interface MeasureStructureResult {
  modules?: ModulesMeasure;
  apps?: AppStructureMeasure;
}

// `query/` is intentionally NOT required. A module can be command-only (e.g.
// event/command-sourced modules like account-payable and accounting-event that
// return `queries: {}` and expose no read side). The rest of the framework
// already treats query as optional — code generation guards on its existence,
// and doc/sync-check match it by glob — so requiring the directory here would
// be the sole inconsistency. Queries are feature-driven: present only when a
// feature implies a read operation.
const REQUIRED_MODULE_DIRS = ["db", "command", "lib"];
const REQUIRED_APP_DIRS = ["backend", "frontend"];

async function dirExists(absPath: string): Promise<boolean> {
  try {
    const stat = await fs.stat(absPath);
    return stat.isDirectory();
  } catch {
    return false;
  }
}

async function measureModules(modulesRoot: string, cwd: string): Promise<ModulesMeasure> {
  const moduleDirs = await fg(`${modulesRoot}/*`, {
    cwd,
    onlyDirectories: true,
    deep: 1,
  });
  const violations: StructureViolation[] = [];

  for (const moduleDir of moduleDirs) {
    const absModule = path.join(cwd, moduleDir);
    for (const required of REQUIRED_MODULE_DIRS) {
      if (!(await dirExists(path.join(absModule, required)))) {
        violations.push({
          type: "missing-required-dir",
          path: path.join(moduleDir, required),
        });
      }
    }
  }

  return { count: moduleDirs.length, violations };
}

async function measureApps(appRoot: string, cwd: string): Promise<AppStructureMeasure> {
  const violations: StructureViolation[] = [];
  const absApp = path.join(cwd, appRoot);
  for (const required of REQUIRED_APP_DIRS) {
    if (!(await dirExists(path.join(absApp, required)))) {
      violations.push({
        type: "missing-required-dir",
        path: path.join(appRoot, required),
      });
    }
  }
  return { count: 1, violations };
}

/**
 * `measure structure` reports filesystem-shape violations:
 *
 * - **naming-violation** — name / placement issues caught by the
 *   in-house rule tree (see `./naming-rules.ts`).
 * - **missing-required-dir** — required directories (`db/`, `command/`,
 *   `lib/` for modules; `backend/`, `frontend/` for apps) absent from a
 *   module/app. `query/` is optional (command-only modules are valid).
 *
 * Content-level checks (resolver imports, direct DB ops, etc.) are
 * intentionally out of scope here.
 */
export async function collectMeasureStructure(
  config: { appRoot?: string; modulesRoot?: string },
  cwd: string,
): Promise<MeasureStructureResult> {
  const result: MeasureStructureResult = {};

  const namingViolations = await findNamingViolations(cwd);
  const modulesNaming: StructureViolation[] = [];
  const appsNaming: StructureViolation[] = [];
  for (const nv of namingViolations) {
    const bucket = classifyViolationBucket(nv.path, config.modulesRoot, config.appRoot);
    const entry: StructureViolation = {
      type: "naming-violation",
      path: nv.path,
      rule: nv.rule,
    };
    if (bucket === "modules") modulesNaming.push(entry);
    else if (bucket === "apps") appsNaming.push(entry);
    // "other" buckets are silently dropped — the rule tree only covers
    // modules/ and apps/, so any other path is outside the rule set.
  }

  if (config.modulesRoot) {
    const measure = await measureModules(config.modulesRoot, cwd);
    measure.violations.push(...modulesNaming);
    result.modules = measure;
  }
  if (config.appRoot) {
    const measure = await measureApps(config.appRoot, cwd);
    measure.violations.push(...appsNaming);
    result.apps = measure;
  }
  return result;
}

export async function runInternalMeasureStructure(
  config: { appRoot?: string; modulesRoot?: string },
  cwd: string,
): Promise<CommandResult> {
  const result = await collectMeasureStructure(config, cwd);
  console.log(formatMeasureStructureReport(result));
  return success();
}

function formatMeasureStructureReport(result: MeasureStructureResult): string {
  return JSON.stringify(result, null, 2);
}
