/**
 * Module Tree Builder — builds a hierarchical module tree from the knowledge graph.
 *
 * Used by CodeWiki to determine documentation generation order:
 * leaf modules first, then parent modules bottom-up.
 */

import type { KnowledgeGraph } from "./graph/knowledge-graph";
import { NodeType } from "./types";

export interface ModuleNode {
  id: string;
  name: string;
  path: string;
  level: "root" | "package" | "subpackage" | "class_group";
  files: string[];
  classes: string[];
  children: ModuleNode[];
  parent?: string;
  docPath?: string;
  status: "pending" | "generating" | "done";
}

/** Maximum number of classes in a leaf module before splitting into class groups. */
const CLASS_GROUP_THRESHOLD = 8;

/** Common naming patterns for grouping classes. */
const CLASS_PATTERNS: [string, RegExp][] = [
  ["Handler", /Handler$/],
  ["Service", /Service$/],
  ["Schema", /Schema$/],
  ["Bean", /Bean$/],
  ["Dto", /Dto$|DTO$/],
  ["Dao", /Dao$|DAO$/],
  ["Controller", /Controller$/],
  ["Util", /Util$|Utils$|Helper$/],
  ["Factory", /Factory$|Builder$|Provider$/],
  ["Exception", /Exception$|Error$/],
  ["Constant", /Constant$|Constants$|Enum$/],
  ["Config", /Config$|Configuration$|Properties$/],
  ["Listener", /Listener$|Observer$/],
  ["Filter", /Filter$|Interceptor$/],
  ["Validator", /Validator$/],
  ["Converter", /Converter$|Transformer$|Adapter$/],
  ["Mapper", /Mapper$/],
  ["Repository", /Repository$/],
  ["Model", /Model$|Entity$|Domain$/],
  ["Manager", /Manager$/],
  ["Client", /Client$|Connector$|Gateway$/],
  ["Test", /Test$|Tests$|Spec$/],
  ["Interface", /^I[A-Z]|Interface$/],
];

/**
 * Finds the longest common prefix among a list of dot-separated package names.
 * Returns the prefix including trailing dot (e.g. "jp.co.kopt.").
 * If there's no common prefix, returns empty string.
 */
export function findCommonPrefix(names: string[]): string {
  if (names.length === 0) return "";
  if (names.length === 1) return "";

  const split = names.map((n) => n.split("."));
  const minLen = Math.min(...split.map((s) => s.length));

  const commonParts: string[] = [];
  for (let i = 0; i < minLen; i++) {
    const part = split[0][i];
    if (split.every((s) => s[i] === part)) {
      commonParts.push(part);
    } else {
      break;
    }
  }

  if (commonParts.length === 0) return "";
  return commonParts.join(".") + ".";
}

/**
 * Groups class names by naming pattern suffix.
 * Returns a map of group name to list of class names.
 * Classes that don't match any pattern go into "Other".
 */
export function groupClassesByPattern(
  classes: string[],
  files: string[],
): Map<string, { classes: string[]; files: string[] }> {
  const classToFile = new Map<string, string>();
  // Build a rough mapping: match class name to file by checking if file path contains the class name
  for (const cls of classes) {
    const simpleName = cls.includes(".") ? cls.split(".").pop()! : cls;
    const matchingFile = files.find((f) => f.includes(simpleName));
    if (matchingFile) {
      classToFile.set(cls, matchingFile);
    }
  }

  const groups = new Map<string, { classes: string[]; files: string[] }>();
  const assigned = new Set<string>();

  for (const [groupName, pattern] of CLASS_PATTERNS) {
    const matching = classes.filter((cls) => {
      const simpleName = cls.includes(".") ? cls.split(".").pop()! : cls;
      return pattern.test(simpleName) && !assigned.has(cls);
    });

    if (matching.length >= 2) {
      const groupFiles = matching
        .map((cls) => classToFile.get(cls))
        .filter((f): f is string => f !== undefined);

      groups.set(groupName, { classes: matching, files: groupFiles });
      for (const cls of matching) assigned.add(cls);
    }
  }

  // Remaining classes go into "Other"
  const remaining = classes.filter((cls) => !assigned.has(cls));
  if (remaining.length > 0) {
    const remainingFiles = remaining
      .map((cls) => classToFile.get(cls))
      .filter((f): f is string => f !== undefined);
    groups.set("Other", { classes: remaining, files: remainingFiles });
  }

  return groups;
}

/**
 * Builds a hierarchical module tree from the knowledge graph.
 *
 * 1. Gets all PACKAGE nodes from the graph
 * 2. Strips common prefix (e.g. "jp.co.kopt.")
 * 3. Groups by top-level package (first segment after prefix)
 * 4. Builds hierarchy from dot-separated names
 * 5. Splits large leaf modules (>15 classes) into class_group children
 */
export function buildModuleTree(graph: KnowledgeGraph): ModuleNode {
  const packageIds = graph.getNodesByType(NodeType.PACKAGE);

  // Collect package info
  const packages: { id: string; name: string }[] = [];
  for (const pkgId of packageIds) {
    const attrs = graph.getNodeAttrs(pkgId);
    if (!attrs.name) continue;
    packages.push({ id: pkgId, name: attrs.name as string });
  }

  const packageNames = packages.map((p) => p.name);
  const commonPrefix = findCommonPrefix(packageNames);
  const commonPrefixBase = commonPrefix.replace(/\.$/, "").split(".").pop() || "application";
  const toRelativePackageName = (packageName: string) => {
    const relative = commonPrefix ? packageName.slice(commonPrefix.length) : packageName;
    return relative || commonPrefixBase;
  };
  const packageInventory = new Map<string, { classes: string[]; files: string[] }>();
  const fileToPackage = new Map<string, string>();

  for (const fileId of graph.getNodesByType(NodeType.FILE)) {
    const attrs = graph.getNodeAttrs(fileId);
    const packageName = typeof attrs.package === "string" ? attrs.package : "";
    const filePath = typeof attrs.file_path === "string" ? attrs.file_path : "";
    if (!packageName || !filePath) {
      continue;
    }

    const current = packageInventory.get(packageName) || { classes: [], files: [] };
    current.files.push(filePath);
    packageInventory.set(packageName, current);
    fileToPackage.set(filePath, packageName);
  }

  for (const classType of [NodeType.CLASS, NodeType.INTERFACE, NodeType.ENUM]) {
    for (const classId of graph.getNodesByType(classType)) {
      const attrs = graph.getNodeAttrs(classId);
      const filePath = typeof attrs.file_path === "string" ? attrs.file_path : "";
      const className = typeof attrs.name === "string" ? attrs.name : "";
      if (!filePath || !className) {
        continue;
      }

      const packageName = fileToPackage.get(filePath);
      if (!packageName) {
        continue;
      }

      const current = packageInventory.get(packageName) || { classes: [], files: [] };
      current.classes.push(className);
      if (!current.files.includes(filePath)) {
        current.files.push(filePath);
      }
      packageInventory.set(packageName, current);
    }
  }

  // Build root node
  const root: ModuleNode = {
    id: "root",
    name: "root",
    path: "",
    level: "root",
    files: [],
    classes: [],
    children: [],
    status: "pending",
  };

  // Group packages by their relative path segments
  const topLevelGroups = new Map<string, typeof packages>();

  for (const pkg of packages) {
    const relative = toRelativePackageName(pkg.name);
    const segments = relative.split(".");
    const topLevel = segments[0];

    if (!topLevelGroups.has(topLevel)) {
      topLevelGroups.set(topLevel, []);
    }
    topLevelGroups.get(topLevel)!.push(pkg);
  }

  // For each top-level group, build subtree
  for (const [topLevel, groupPackages] of topLevelGroups) {
    const topNode: ModuleNode = {
      id: `pkg:${topLevel}`,
      name: topLevel,
      path: topLevel,
      level: "package",
      docPath: `${topLevel}.md`,
      files: [],
      classes: [],
      children: [],
      parent: "root",
      status: "pending",
    };

    // Build sub-hierarchy within this top-level group
    const subNodes = new Map<string, ModuleNode>();
    subNodes.set(topLevel, topNode);

    for (const pkg of groupPackages) {
      const relative = toRelativePackageName(pkg.name);
      const segments = relative.split(".");

      const packageContents = packageInventory.get(pkg.name) || { classes: [], files: [] };

      // Ensure all intermediate nodes exist
      let currentPath = segments[0];
      for (let i = 1; i < segments.length; i++) {
        const parentPath = currentPath;
        currentPath = currentPath + "." + segments[i];

        if (!subNodes.has(currentPath)) {
          const subNode: ModuleNode = {
            id: `pkg:${currentPath}`,
            name: segments[i],
            path: currentPath,
            level: "subpackage",
            docPath: `${currentPath.replace(/\./g, "/")}.md`,
            files: [],
            classes: [],
            children: [],
            parent: `pkg:${parentPath}`,
            status: "pending",
          };
          subNodes.set(currentPath, subNode);

          const parentNode = subNodes.get(parentPath)!;
          parentNode.children.push(subNode);
        }
      }

      // Attach classes and files to the leaf package node
      const leafNode = subNodes.get(relative)!;
      leafNode.classes.push(...packageContents.classes);
      leafNode.files = [...new Set([...leafNode.files, ...packageContents.files])];
    }

    // Split large leaf modules into class groups
    splitLargeModules(topNode);

    root.children.push(topNode);
  }

  return root;
}

/**
 * Recursively splits leaf modules with more than CLASS_GROUP_THRESHOLD classes
 * into class_group children by naming pattern.
 */
function splitLargeModules(node: ModuleNode): void {
  // Process children first (depth-first)
  for (const child of node.children) {
    splitLargeModules(child);
  }

  // Only split leaf nodes with many classes
  if (node.children.length === 0 && node.classes.length > CLASS_GROUP_THRESHOLD) {
    const groups = groupClassesByPattern(node.classes, node.files);

    for (const [groupName, { classes, files }] of groups) {
      const groupNode: ModuleNode = {
        id: `${node.id}:${groupName}`,
        name: groupName,
        path: `${node.path}/${groupName}`,
        level: "class_group",
        // Namespace class-group docs so they cannot collide with real
        // subpackage docs such as foo.Bar -> foo/Bar.md.
        docPath: `${node.path.replace(/\./g, "/")}/_groups/${groupName}.md`,
        files: [...new Set(files)],
        classes,
        children: [],
        parent: node.id,
        status: "pending",
      };
      node.children.push(groupNode);
    }

    // Clear parent classes/files to avoid double-counting
    node.classes = [];
    node.files = [];
  }
}

/**
 * Collects leaf modules (no children, not root) in depth-first order.
 * These are the modules that should be documented first.
 */
export function getLeafModules(root: ModuleNode): ModuleNode[] {
  const leaves: ModuleNode[] = [];

  function walk(node: ModuleNode): void {
    if (node.level !== "root" && node.children.length === 0) {
      leaves.push(node);
      return;
    }
    for (const child of node.children) {
      walk(child);
    }
  }

  walk(root);
  return leaves;
}

/**
 * Collects parent modules (has children, not root) in bottom-up order.
 * These modules should be documented after their children.
 */
export function getParentModulesBottomUp(root: ModuleNode): ModuleNode[] {
  const parents: ModuleNode[] = [];

  function walk(node: ModuleNode): void {
    // Recurse into children first (bottom-up)
    for (const child of node.children) {
      walk(child);
    }
    // Collect this node if it has children and is not root
    if (node.level !== "root" && node.children.length > 0) {
      parents.push(node);
    }
  }

  walk(root);
  return parents;
}
