import path from "node:path";
import fg from "fast-glob";

/**
 * Filesystem naming rules for `measure structure`.
 *
 * Earlier iterations delegated to `@ls-lint/ls-lint` (= a Go binary).
 * That worked but coupled a first-party tool to a fairly large external
 * dependency for the sake of ~4 matchers we actually used. The rule set
 * is small and stable enough to own outright; this module is the result.
 *
 * Shape mirrors the prior YAML config:
 *
 *   modules:
 *     .dir applies to immediate children of `modules/`
 *     "*"  applies inside each module
 *       db/.ts, command/.ts, query/.ts, lib/.ts, ... etc.
 *
 *   apps:
 *     .dir applies to immediate children of `apps/`
 *     "*"  applies inside each app
 *       .dir restricts which sub-dirs are allowed under an app root
 *       backend/src/resolver/.ts, frontend/src/components/.tsx
 */

export type Matcher = "kebab-case" | "camelCase" | "PascalCase" | { regex: string };

interface RuleNode {
  match?: Partial<Record<string, Matcher>>; // keys: ".dir" or ".<ext>"
  children?: Record<string, RuleNode>; // key "*" = wildcard segment
}

const MODULE_LEAF_FILES: Matcher = {
  regex: "^(module|index|tailor\\.config|vitest\\.config)$",
};
const COMMAND_LIKE_TS: Matcher = {
  regex: "^[a-z][a-zA-Z0-9]*(\\.test|\\.generated)?$",
};
const LIB_TS: Matcher = {
  regex: "^(types|_[a-zA-Z0-9_]+|.*\\.generated)$",
};
const APP_SUBDIR: Matcher = { regex: "^(backend|frontend|e2e|docs)$" };
const RESOLVER_TS: Matcher = { regex: "^[a-z][a-zA-Z0-9]*(\\.test)?$" };

const RULES: RuleNode = {
  children: {
    modules: {
      match: { ".dir": "kebab-case" },
      children: {
        "*": {
          match: { ".ts": MODULE_LEAF_FILES },
          children: {
            db: { match: { ".ts": "camelCase" } },
            command: { match: { ".ts": COMMAND_LIKE_TS } },
            query: { match: { ".ts": COMMAND_LIKE_TS } },
            lib: { match: { ".ts": LIB_TS } },
            executor: { match: { ".ts": "camelCase" } },
            seed: { match: { ".ts": "camelCase" } },
            testing: { match: { ".ts": "camelCase" } },
          },
        },
      },
    },
    apps: {
      match: { ".dir": "kebab-case" },
      children: {
        "*": {
          match: { ".dir": APP_SUBDIR },
          children: {
            backend: {
              children: {
                src: {
                  children: {
                    resolver: { match: { ".ts": RESOLVER_TS } },
                  },
                },
              },
            },
            frontend: {
              children: {
                src: {
                  children: {
                    components: { match: { ".tsx": "PascalCase" } },
                  },
                },
              },
            },
          },
        },
      },
    },
  },
};

const IGNORE_GLOBS = [
  "**/node_modules/**",
  "**/dist/**",
  "**/generated/**",
  "**/.next/**",
  "**/.turbo/**",
  "**/coverage/**",
];

export interface NamingViolation {
  path: string;
  rule: string;
}

function applyMatcher(m: Matcher, name: string): boolean {
  if (typeof m === "string") {
    switch (m) {
      case "kebab-case":
        return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(name);
      case "camelCase":
        return /^[a-z][a-zA-Z0-9]*$/.test(name);
      case "PascalCase":
        return /^[A-Z][a-zA-Z0-9]*$/.test(name);
    }
  }
  return new RegExp(m.regex).test(name);
}

function formatMatcher(key: string, m: Matcher): string {
  if (typeof m === "string") return `${key}:${m}`;
  return `${key}:regex:${m.regex}`;
}

/**
 * Walks the rule tree along the given parent segments and returns the
 * rule node that applies to a child entry. Falls back to "*" at each
 * level when no literal segment match exists. Returns `null` when the
 * path runs deeper than the rule tree defines (= no naming rules apply
 * at that depth).
 */
function resolveNode(parentSegments: string[]): RuleNode | null {
  let node: RuleNode = RULES;
  for (const seg of parentSegments) {
    const next = node.children?.[seg] ?? node.children?.["*"];
    if (!next) return null;
    node = next;
  }
  return node;
}

function splitNameExt(basename: string): { stem: string; ext: string } {
  if (basename.endsWith(".d.ts")) {
    return { stem: basename.slice(0, -".d.ts".length), ext: ".d.ts" };
  }
  const idx = basename.lastIndexOf(".");
  if (idx <= 0) return { stem: basename, ext: "" };
  return { stem: basename.slice(0, idx), ext: basename.slice(idx) };
}

export async function findNamingViolations(cwd: string): Promise<NamingViolation[]> {
  const violations: NamingViolation[] = [];

  const [dirs, files] = await Promise.all([
    fg(["modules/**", "apps/**"], {
      cwd,
      onlyDirectories: true,
      ignore: IGNORE_GLOBS,
    }),
    fg(["modules/**", "apps/**"], {
      cwd,
      onlyFiles: true,
      ignore: IGNORE_GLOBS,
    }),
  ]);

  for (const dir of dirs) {
    const segments = dir.split("/");
    const parentSegments = segments.slice(0, -1);
    const name = segments[segments.length - 1] ?? "";
    const node = resolveNode(parentSegments);
    const matcher = node?.match?.[".dir"];
    if (matcher && !applyMatcher(matcher, name)) {
      violations.push({ path: dir, rule: formatMatcher(".dir", matcher) });
    }
  }

  for (const file of files) {
    const segments = file.split("/");
    const parentSegments = segments.slice(0, -1);
    const basename = segments[segments.length - 1] ?? "";
    const { stem, ext } = splitNameExt(basename);
    if (!ext) continue;
    const node = resolveNode(parentSegments);
    const matcher = node?.match?.[ext];
    if (matcher && !applyMatcher(matcher, stem)) {
      violations.push({ path: file, rule: formatMatcher(ext, matcher) });
    }
  }

  return violations.sort((a, b) => a.path.localeCompare(b.path));
}

export function classifyViolationBucket(
  violationPath: string,
  modulesRoot: string | undefined,
  appRoot: string | undefined,
): "modules" | "apps" | "other" {
  if (
    modulesRoot &&
    violationPath.startsWith(`${modulesRoot}${path.sep === "\\" ? path.sep : "/"}`)
  )
    return "modules";
  if (appRoot && violationPath.startsWith(`${appRoot}/`)) return "apps";
  if (violationPath.startsWith("modules/")) return "modules";
  if (violationPath.startsWith("apps/")) return "apps";
  return "other";
}
