/**
 * Cross-cutting pattern detection, import analysis, technology stack
 * detection, key class discovery, and resource file discovery.
 */

import { join } from "node:path";
import { readdirSync, statSync } from "node:fs";
import { KnowledgeGraph } from "../../repopilot/graph/knowledge-graph";
import type { ModuleNode } from "../../repopilot/module-tree";
import { EdgeType, NodeType } from "../../repopilot/types";
import type { CrossCuttingPattern, KeyClassCandidate } from "../types";
import {
  CAP_CROSS_CUTTING_PAGES,
  CAP_DEEP_DIVE_PAGES,
} from "../constants";
import {
  collectModuleFiles,
  getNumber,
  getString,
  toRelativePath,
  uniqueStrings,
} from "../utils";
import { getClassEntries } from "./graph-extractor";

/* ------------------------------------------------------------------ */
/*  Cross-module relationship evidence                                 */
/* ------------------------------------------------------------------ */

export function buildCrossModuleRelationships(
  module: ModuleNode,
  graph: KnowledgeGraph,
  tree: ModuleNode,
  sourcePath: string,
): string {
  const moduleFiles = new Set(collectModuleFiles(module));
  const classes = getClassEntries(graph, moduleFiles);
  const classNodeIds = new Set(classes.map((c) => c.nodeId));

  const inbound = new Map<string, string[]>();
  const outbound = new Map<string, string[]>();

  for (const classEntry of classes) {
    // Outbound: who does this class reference/call outside the module?
    for (const edgeType of [EdgeType.REFERENCES, EdgeType.CALLS]) {
      for (const targetId of graph.getNeighbors(
        classEntry.nodeId,
        edgeType,
        "out",
      )) {
        if (classNodeIds.has(targetId)) continue;
        if (!graph.hasNode(targetId)) continue;
        const attrs = graph.getNodeAttrs(targetId);
        const name = getString(attrs, "name");
        if (name) {
          const key = name;
          if (!outbound.has(key)) outbound.set(key, []);
          outbound.get(key)!.push(classEntry.name);
        }
      }
    }

    // Inbound: who references/calls this class from outside the module?
    for (const edgeType of [EdgeType.REFERENCES, EdgeType.CALLS]) {
      for (const callerId of graph.getNeighbors(
        classEntry.nodeId,
        edgeType,
        "in",
      )) {
        if (classNodeIds.has(callerId)) continue;
        if (!graph.hasNode(callerId)) continue;
        const attrs = graph.getNodeAttrs(callerId);
        const name = getString(attrs, "name");
        if (name) {
          const key = name;
          if (!inbound.has(key)) inbound.set(key, []);
          inbound.get(key)!.push(classEntry.name);
        }
      }
    }
  }

  const lines: string[] = [];

  if (inbound.size > 0) {
    lines.push("### Used by other modules (inbound)");
    for (const [externalClass, localClasses] of [...inbound.entries()].slice(
      0,
      12,
    )) {
      lines.push(
        `- ${externalClass} uses ${uniqueStrings(localClasses).slice(0, 4).join(", ")}`,
      );
    }
  }

  if (outbound.size > 0) {
    lines.push("### Depends on other modules (outbound)");
    for (const [externalClass, localClasses] of [
      ...outbound.entries(),
    ].slice(0, 12)) {
      lines.push(
        `- ${uniqueStrings(localClasses).slice(0, 4).join(", ")} use ${externalClass}`,
      );
    }
  }

  return lines.length > 0
    ? lines.join("\n")
    : "No cross-module relationships were detected in the index.";
}

/* ------------------------------------------------------------------ */
/*  Import pattern / technology detection                              */
/* ------------------------------------------------------------------ */

export function detectImportPatterns(
  graph: KnowledgeGraph,
  moduleFiles?: Set<string>,
): string[] {
  const importCounts = new Map<string, number>();

  for (const fileId of graph.getNodesByType(NodeType.FILE)) {
    if (moduleFiles) {
      const attrs = graph.getNodeAttrs(fileId);
      const fp = getString(attrs, "file_path");
      if (!fp || !moduleFiles.has(fp)) continue;
    }

    for (const targetId of graph.getNeighbors(
      fileId,
      EdgeType.IMPORTS,
      "out",
    )) {
      if (!graph.hasNode(targetId)) continue;
      const attrs = graph.getNodeAttrs(targetId);
      const name = getString(attrs, "name") || targetId;
      // Extract top-level package prefix (e.g., "javax.ejb" from "javax.ejb.SessionBean")
      const parts = name.split(".");
      const prefix =
        parts.length >= 2 ? `${parts[0]}.${parts[1]}` : parts[0];
      importCounts.set(prefix, (importCounts.get(prefix) || 0) + 1);
    }
  }

  return [...importCounts.entries()]
    .sort((a, b) => b[1] - a[1])
    .slice(0, 20)
    .map(([prefix, count]) => `${prefix} (${count} imports)`);
}

export function buildImportPatternEvidence(
  graph: KnowledgeGraph,
  moduleFiles: Set<string>,
): string {
  const patterns = detectImportPatterns(graph, moduleFiles);
  if (patterns.length === 0) {
    return "No import patterns detected.";
  }
  return patterns.map((p) => `- ${p}`).join("\n");
}

export function buildTechnologyStack(graph: KnowledgeGraph): string {
  const patterns = detectImportPatterns(graph);

  const knownFrameworks: [string, RegExp][] = [
    ["Java EE / Jakarta EE (EJB)", /javax\.ejb|jakarta\.ejb/],
    ["JPA / Persistence", /javax\.persistence|jakarta\.persistence/],
    ["Servlet / HTTP", /javax\.servlet|jakarta\.servlet/],
    ["JAX-RS / REST", /javax\.ws\.rs|jakarta\.ws\.rs/],
    ["JMS / Messaging", /javax\.jms|jakarta\.jms/],
    ["Spring Framework", /org\.springframework/],
    ["Spring Boot", /org\.springframework\.boot/],
    ["Hibernate", /org\.hibernate/],
    ["Apache Kafka", /org\.apache\.kafka/],
    ["Apache Commons", /org\.apache\.commons/],
    ["SLF4J / Logging", /org\.slf4j|java\.util\.logging/],
    ["Jackson / JSON", /com\.fasterxml\.jackson/],
    ["JUnit / Testing", /org\.junit|junit\./],
    ["Mockito", /org\.mockito/],
    ["Lombok", /lombok\./],
    ["JDBC / SQL", /java\.sql/],
  ];

  const detected: string[] = [];
  const patternStr = patterns.join(" ");
  for (const [label, regex] of knownFrameworks) {
    if (regex.test(patternStr)) {
      detected.push(label);
    }
  }

  if (detected.length === 0) {
    return "No well-known frameworks detected from import analysis.";
  }
  return detected.map((d) => `- ${d}`).join("\n");
}

/* ------------------------------------------------------------------ */
/*  DocMap planning — cross-cutting patterns & key classes             */
/* ------------------------------------------------------------------ */

export function detectCrossCuttingPatterns(
  graph: KnowledgeGraph,
  tree: ModuleNode,
): CrossCuttingPattern[] {
  const allFiles = new Set(
    tree.children.flatMap((child) => collectModuleFiles(child)),
  );
  const allClasses = getClassEntries(graph, allFiles);

  const patternDefs: {
    id: string;
    title: string;
    desc: string;
    classRegex: RegExp;
    importRegex: RegExp;
  }[] = [
    {
      id: "data-access",
      title: "Data Access Patterns",
      desc: "How the codebase reads and writes persistent data.",
      classRegex: /Dao|DAO|Repository|Persistence|DataSource|JdbcTemplate/i,
      importRegex: /javax\.persistence|java\.sql|hibernate|jpa/i,
    },
    {
      id: "messaging",
      title: "Message Handling",
      desc: "Asynchronous messaging, event handling, and queue integration.",
      classRegex: /Message|Handler|Listener|Consumer|Producer|Event|Queue/i,
      importRegex: /javax\.jms|kafka|amqp|messaging/i,
    },
    {
      id: "configuration",
      title: "Configuration Architecture",
      desc: "How the application is configured, including properties, environment settings, and feature flags.",
      classRegex: /Config|Configuration|Properties|Settings|Environment/i,
      importRegex: /springframework\.context|Properties|Configuration/i,
    },
    {
      id: "api-contracts",
      title: "API Contracts and Endpoints",
      desc: "REST endpoints, service interfaces, and external API contracts.",
      classRegex: /Controller|Resource|Endpoint|RestApi|Servlet/i,
      importRegex: /javax\.ws\.rs|javax\.servlet|springframework\.web/i,
    },
    {
      id: "error-handling",
      title: "Error Handling and Resilience",
      desc: "Exception handling patterns, retry logic, and error recovery.",
      classRegex: /Exception|Error|Fallback|Retry|CircuitBreaker/i,
      importRegex: /Exception/i,
    },
  ];

  const results: CrossCuttingPattern[] = [];

  for (const def of patternDefs) {
    const matchingClasses = allClasses.filter(
      (c) => def.classRegex.test(c.name) || def.classRegex.test(c.type),
    );
    const importPatterns = detectImportPatterns(graph, allFiles);
    const matchingImports = importPatterns.filter((p) =>
      def.importRegex.test(p),
    );

    // Only create a page if we have meaningful evidence
    if (matchingClasses.length >= 3 || matchingImports.length >= 2) {
      results.push({
        id: def.id,
        title: def.title,
        description: def.desc,
        nodeIds: matchingClasses.map((c) => c.nodeId),
        files: uniqueStrings(matchingClasses.map((c) => c.filePath)),
        importSignals: matchingImports,
      });
    }
  }

  return results.slice(0, CAP_CROSS_CUTTING_PAGES);
}

export function findKeyClassCandidates(
  graph: KnowledgeGraph,
  tree: ModuleNode,
  threshold = 6,
): KeyClassCandidate[] {
  const allFiles = new Set(
    tree.children.flatMap((child) => collectModuleFiles(child)),
  );
  const candidates: KeyClassCandidate[] = [];

  for (const nodeType of [
    NodeType.CLASS,
    NodeType.INTERFACE,
    NodeType.ENUM,
  ]) {
    for (const nodeId of graph.getNodesByType(nodeType)) {
      const attrs = graph.getNodeAttrs(nodeId);
      const fp = getString(attrs, "file_path");
      if (!fp || !allFiles.has(fp)) continue;

      // Count inbound connections (who depends on this class)
      let connectionCount = 0;
      for (const edgeType of [
        EdgeType.REFERENCES,
        EdgeType.CALLS,
        EdgeType.IMPLEMENTS,
        EdgeType.EXTENDS,
      ]) {
        connectionCount += graph.getNeighbors(
          nodeId,
          edgeType,
          "in",
        ).length;
      }

      if (connectionCount >= threshold) {
        candidates.push({
          nodeId,
          name: String(attrs.name),
          filePath: fp,
          connectionCount,
          lineStart: getNumber(attrs, "line_start"),
          lineEnd: getNumber(attrs, "line_end"),
        });
      }
    }
  }

  return candidates
    .sort((a, b) => b.connectionCount - a.connectionCount)
    .slice(0, CAP_DEEP_DIVE_PAGES);
}

/** Discover non-Java resource files (XML, properties, YAML, SQL). */
export function discoverResourceFiles(sourcePath: string): string[] {
  const resourceExts = new Set([
    ".xml",
    ".properties",
    ".yaml",
    ".yml",
    ".sql",
    ".json",
    ".conf",
    ".cfg",
  ]);
  const results: string[] = [];

  function walk(dir: string, depth: number) {
    if (depth > 6 || results.length > 40) return;
    let entries: string[];
    try {
      entries = readdirSync(dir);
    } catch {
      return;
    }
    for (const entry of entries) {
      if (
        entry.startsWith(".") ||
        entry === "node_modules" ||
        entry === "target" ||
        entry === "build"
      )
        continue;
      const full = join(dir, entry);
      let stat;
      try {
        stat = statSync(full);
      } catch {
        continue;
      }
      if (stat.isDirectory()) {
        walk(full, depth + 1);
      } else {
        const ext = entry.slice(entry.lastIndexOf(".")).toLowerCase();
        if (resourceExts.has(ext)) {
          results.push(toRelativePath(sourcePath, full));
        }
      }
    }
  }

  walk(sourcePath, 0);
  return results.slice(0, 30);
}
