/**
 * Extract graph nodes and edges from tree-sitter Java ASTs.
 */

import { makeFileNode, makeNodeId, makePackageNode } from "../graph/model";
import { getNodeText, type TSNode, type TSTree } from "./parser";
import { EdgeType, type GraphEdge, type GraphNode, NodeType, type SourceLocation } from "../types";

export function extractFromFile(
  tree: TSTree,
  filePath: string,
  source: string,
  includeSnippets = true,
): { nodes: GraphNode[]; edges: GraphEdge[] } {
  const root = tree.rootNode;
  const nodes: GraphNode[] = [];
  const edges: GraphEdge[] = [];

  // Reset per-file method dedup tracker
  resetMethodDedup();

  // Extract package
  const pkg = extractPackage(root, source);

  // Count lines
  const loc = source.split("\n").length;

  // File node
  const fileNode = makeFileNode(filePath, pkg, loc);
  nodes.push(fileNode);

  // Package node
  if (pkg) {
    const pkgNode = makePackageNode(pkg);
    nodes.push(pkgNode);
    edges.push({
      source: fileNode.id,
      target: pkgNode.id,
      type: EdgeType.CONTAINS,
      attributes: {},
    });
  }

  // Extract imports
  const imports = extractImports(root, source);

  // Extract class declarations
  for (const classTs of findNodesByType(root, "class_declaration")) {
    const { nodes: cn, edges: ce } = extractClass(classTs, filePath, fileNode.id, source, includeSnippets);
    nodes.push(...cn);
    edges.push(...ce);
  }

  // Extract interface declarations
  for (const ifaceTs of findNodesByType(root, "interface_declaration")) {
    const { nodes: in_, edges: ie } = extractInterface(ifaceTs, filePath, fileNode.id, source);
    nodes.push(...in_);
    edges.push(...ie);
  }

  // Extract enum declarations
  for (const enumTs of findNodesByType(root, "enum_declaration")) {
    const { nodes: en, edges: ee } = extractEnum(enumTs, filePath, fileNode.id, source);
    nodes.push(...en);
    edges.push(...ee);
  }

  // Store imports as placeholder edges (resolved later)
  for (const imp of imports) {
    edges.push({
      source: fileNode.id,
      target: `import:${imp}`,
      type: EdgeType.IMPORTS,
      attributes: {},
    });
  }

  return { nodes, edges };
}

function extractPackage(root: TSNode, source: string): string {
  for (const child of root.children) {
    if (child.type === "package_declaration") {
      for (const sub of child.children) {
        if (sub.type === "scoped_identifier") {
          return getNodeText(sub, source);
        }
      }
    }
  }
  return "";
}

function extractImports(root: TSNode, source: string): string[] {
  const imports: string[] = [];
  for (const child of root.children) {
    if (child.type === "import_declaration") {
      for (const sub of child.children) {
        if (sub.type === "scoped_identifier" || sub.type === "identifier") {
          imports.push(getNodeText(sub, source));
        }
      }
    }
  }
  return imports;
}

function extractClass(
  node: TSNode,
  filePath: string,
  parentId: string,
  source: string,
  includeSnippets: boolean,
): { nodes: GraphNode[]; edges: GraphEdge[] } {
  const nodes: GraphNode[] = [];
  const edges: GraphEdge[] = [];

  const name = getChildText(node, "identifier", source);
  if (!name) return { nodes, edges };

  const classId = makeNodeId(filePath, name, NodeType.CLASS);
  const location: SourceLocation = {
    filePath,
    lineStart: node.startPosition.row + 1,
    lineEnd: node.endPosition.row + 1,
  };

  const modifiers = extractModifiers(node, source);
  const superclass = getSuperclass(node, source);
  const interfaces = getInterfaces(node, source);

  const attrs: Record<string, unknown> = { modifiers };
  if (superclass) attrs.extends = superclass;
  if (interfaces.length > 0) attrs.implements = interfaces;
  if (includeSnippets) {
    const snippetEnd = Math.min(node.startIndex + 2000, node.endIndex);
    attrs.snippet = source.slice(node.startIndex, snippetEnd);
  }

  nodes.push({ id: classId, type: NodeType.CLASS, name, location, attributes: attrs });
  edges.push({ source: parentId, target: classId, type: EdgeType.CONTAINS, attributes: {} });

  if (superclass) {
    edges.push({
      source: classId,
      target: `class_ref:${superclass}`,
      type: EdgeType.EXTENDS,
      attributes: {},
    });
  }
  for (const iface of interfaces) {
    edges.push({
      source: classId,
      target: `class_ref:${iface}`,
      type: EdgeType.IMPLEMENTS,
      attributes: {},
    });
  }

  // Extract members from class body
  const body = getChildByType(node, "class_body");
  if (body) {
    const { nodes: mn, edges: me } = extractMembers(body, filePath, classId, source, includeSnippets);
    nodes.push(...mn);
    edges.push(...me);
  }

  return { nodes, edges };
}

function extractInterface(
  node: TSNode,
  filePath: string,
  parentId: string,
  source: string,
): { nodes: GraphNode[]; edges: GraphEdge[] } {
  const nodes: GraphNode[] = [];
  const edges: GraphEdge[] = [];

  const name = getChildText(node, "identifier", source);
  if (!name) return { nodes, edges };

  const ifaceId = makeNodeId(filePath, name, NodeType.INTERFACE);
  const location: SourceLocation = {
    filePath,
    lineStart: node.startPosition.row + 1,
    lineEnd: node.endPosition.row + 1,
  };

  nodes.push({
    id: ifaceId,
    type: NodeType.INTERFACE,
    name,
    location,
    attributes: { modifiers: extractModifiers(node, source) },
  });
  edges.push({ source: parentId, target: ifaceId, type: EdgeType.CONTAINS, attributes: {} });

  return { nodes, edges };
}

function extractEnum(
  node: TSNode,
  filePath: string,
  parentId: string,
  source: string,
): { nodes: GraphNode[]; edges: GraphEdge[] } {
  const nodes: GraphNode[] = [];
  const edges: GraphEdge[] = [];

  const name = getChildText(node, "identifier", source);
  if (!name) return { nodes, edges };

  const enumId = makeNodeId(filePath, name, NodeType.ENUM);
  const location: SourceLocation = {
    filePath,
    lineStart: node.startPosition.row + 1,
    lineEnd: node.endPosition.row + 1,
  };

  nodes.push({
    id: enumId,
    type: NodeType.ENUM,
    name,
    location,
    attributes: { modifiers: extractModifiers(node, source) },
  });
  edges.push({ source: parentId, target: enumId, type: EdgeType.CONTAINS, attributes: {} });

  return { nodes, edges };
}

function extractMembers(
  body: TSNode,
  filePath: string,
  classId: string,
  source: string,
  includeSnippets: boolean,
): { nodes: GraphNode[]; edges: GraphEdge[] } {
  const nodes: GraphNode[] = [];
  const edges: GraphEdge[] = [];

  for (let idx = 0; idx < body.children.length; idx++) {
    const child = body.children[idx];
    if (child.type === "method_declaration") {
      const javadoc = findPrecedingJavadoc(body, idx, source);
      const r = extractMethod(child, filePath, classId, source, includeSnippets, javadoc);
      nodes.push(...r.nodes);
      edges.push(...r.edges);
    } else if (child.type === "constructor_declaration") {
      const r = extractConstructor(child, filePath, classId, source, includeSnippets);
      nodes.push(...r.nodes);
      edges.push(...r.edges);
    } else if (child.type === "field_declaration") {
      const r = extractField(child, filePath, classId, source);
      nodes.push(...r.nodes);
      edges.push(...r.edges);
    }
  }

  return { nodes, edges };
}

/**
 * Look backwards from a method declaration to find the preceding Javadoc
 * block comment. Returns the first description line (before @param tags).
 */
function findPrecedingJavadoc(
  body: TSNode,
  methodIdx: number,
  source: string,
): string | undefined {
  // Walk backwards up to 4 siblings to find a block_comment
  for (let i = methodIdx - 1; i >= Math.max(0, methodIdx - 4); i--) {
    const prev = body.children[i];
    if (prev.type === "block_comment") {
      const text = getNodeText(prev, source);
      if (!text.startsWith("/**")) continue; // not Javadoc
      // Extract description lines (after /** and before @param)
      const descLines = text
        .split("\n")
        .filter(
          (l) =>
            l.includes("*") &&
            !l.includes("/**") &&
            !l.includes("*/") &&
            !l.trim().startsWith("* @"),
        )
        .map((l) => l.replace(/^\s*\*\s*/, "").trim())
        .filter(Boolean);
      return descLines[0] || undefined;
    }
    // Skip annotations (modifiers) between comment and method
    if (prev.type !== "modifiers" && prev.type !== "marker_annotation" && prev.type !== "annotation") {
      break; // something else in between — no Javadoc for this method
    }
  }
  return undefined;
}

/** Track method IDs seen so far to disambiguate overloads within a file. */
const _seenMethodIds = new Set<string>();

/** Reset the per-file dedup set. Call before processing each file. */
export function resetMethodDedup(): void {
  _seenMethodIds.clear();
}

function extractMethod(
  node: TSNode,
  filePath: string,
  classId: string,
  source: string,
  includeSnippets: boolean,
  javadoc?: string,
): { nodes: GraphNode[]; edges: GraphEdge[] } {
  const nodes: GraphNode[] = [];
  const edges: GraphEdge[] = [];

  const name = getChildText(node, "identifier", source);
  if (!name) return { nodes, edges };

  const className = classId.split(":").pop() || "";
  let methodId = makeNodeId(filePath, `${className}.${name}`, NodeType.METHOD);

  // Disambiguate overloaded methods by appending line number
  if (_seenMethodIds.has(methodId)) {
    const line = node.startPosition.row + 1;
    methodId = makeNodeId(filePath, `${className}.${name}_L${line}`, NodeType.METHOD);
  }
  _seenMethodIds.add(methodId);

  const location: SourceLocation = {
    filePath,
    lineStart: node.startPosition.row + 1,
    lineEnd: node.endPosition.row + 1,
  };

  const attrs: Record<string, unknown> = {
    modifiers: extractModifiers(node, source),
    return_type: getReturnType(node, source),
    parameters: getParameters(node, source),
  };
  if (javadoc) {
    attrs.javadoc = javadoc;
  }
  const invokedMethods = getInvokedMethods(node, source);
  if (invokedMethods.length > 0) {
    attrs.invoked_methods = invokedMethods;
  }
  if (includeSnippets) {
    const snippetEnd = Math.min(node.startIndex + 1000, node.endIndex);
    attrs.snippet = source.slice(node.startIndex, snippetEnd);
  }

  nodes.push({ id: methodId, type: NodeType.METHOD, name, location, attributes: attrs });
  edges.push({ source: classId, target: methodId, type: EdgeType.CONTAINS, attributes: {} });
  for (const invokedMethod of invokedMethods) {
    edges.push({
      source: methodId,
      target: `method_ref:${invokedMethod}`,
      type: EdgeType.CALLS,
      attributes: {},
    });
  }

  return { nodes, edges };
}

function extractConstructor(
  node: TSNode,
  filePath: string,
  classId: string,
  source: string,
  _includeSnippets: boolean,
): { nodes: GraphNode[]; edges: GraphEdge[] } {
  const nodes: GraphNode[] = [];
  const edges: GraphEdge[] = [];

  const name = getChildText(node, "identifier", source);
  if (!name) return { nodes, edges };

  const ctorId = makeNodeId(filePath, `${name}.<init>`, NodeType.CONSTRUCTOR);
  const location: SourceLocation = {
    filePath,
    lineStart: node.startPosition.row + 1,
    lineEnd: node.endPosition.row + 1,
  };

  nodes.push({
    id: ctorId,
    type: NodeType.CONSTRUCTOR,
    name,
    location,
    attributes: { parameters: getParameters(node, source) },
  });
  edges.push({ source: classId, target: ctorId, type: EdgeType.CONTAINS, attributes: {} });

  return { nodes, edges };
}

function extractField(
  node: TSNode,
  filePath: string,
  classId: string,
  source: string,
): { nodes: GraphNode[]; edges: GraphEdge[] } {
  const nodes: GraphNode[] = [];
  const edges: GraphEdge[] = [];

  for (const child of node.children) {
    if (child.type !== "variable_declarator") continue;

    const name = getChildText(child, "identifier", source);
    if (!name) continue;

    const className = classId.split(":").pop() || "";
    const fieldId = makeNodeId(filePath, `${className}.${name}`, NodeType.FIELD);
    const location: SourceLocation = {
      filePath,
      lineStart: node.startPosition.row + 1,
      lineEnd: node.endPosition.row + 1,
    };

    const modifiers = extractModifiers(node, source);
    const fieldType = getFieldType(node, source);

    let value: string | undefined;
    if (modifiers.includes("static") && modifiers.includes("final")) {
      value = getFieldValue(child, source);
    }

    const attrs: Record<string, unknown> = { modifiers, field_type: fieldType };
    if (value !== undefined) attrs.value = value;

    nodes.push({ id: fieldId, type: NodeType.FIELD, name, location, attributes: attrs });
    edges.push({ source: classId, target: fieldId, type: EdgeType.CONTAINS, attributes: {} });
  }

  return { nodes, edges };
}

// --- Helper functions ---

function findNodesByType(root: TSNode, typeName: string): TSNode[] {
  return root.children.filter((c) => c.type === typeName);
}

function getChildByType(node: TSNode, typeName: string): TSNode | undefined {
  return node.children.find((c) => c.type === typeName);
}

function getChildText(node: TSNode, typeName: string, source: string): string {
  const child = getChildByType(node, typeName);
  return child ? getNodeText(child, source) : "";
}

function extractModifiers(node: TSNode, source: string): string[] {
  const modifiers: string[] = [];
  for (const child of node.children) {
    if (child.type === "modifiers") {
      for (const mod of child.children) {
        modifiers.push(getNodeText(mod, source));
      }
    }
  }
  return modifiers;
}

function getSuperclass(node: TSNode, source: string): string {
  for (const child of node.children) {
    if (child.type === "superclass") {
      for (const sub of child.children) {
        if (sub.type === "type_identifier" || sub.type === "scoped_type_identifier") {
          return getNodeText(sub, source);
        }
      }
    }
  }
  return "";
}

function getInterfaces(node: TSNode, source: string): string[] {
  const interfaces: string[] = [];
  for (const child of node.children) {
    if (child.type === "super_interfaces") {
      for (const sub of child.children) {
        if (sub.type === "type_list") {
          for (const t of sub.children) {
            if (t.type === "type_identifier" || t.type === "scoped_type_identifier") {
              interfaces.push(getNodeText(t, source));
            }
          }
        }
      }
    }
  }
  return interfaces;
}

const RETURN_TYPE_NODES = new Set([
  "void_type", "type_identifier", "scoped_type_identifier",
  "integral_type", "floating_point_type", "boolean_type",
  "array_type", "generic_type",
]);

function getReturnType(node: TSNode, source: string): string {
  for (const child of node.children) {
    if (RETURN_TYPE_NODES.has(child.type)) {
      return getNodeText(child, source);
    }
  }
  return "";
}

function getParameters(node: TSNode, source: string): string[] {
  const params: string[] = [];
  for (const child of node.children) {
    if (child.type === "formal_parameters") {
      for (const param of child.children) {
        if (param.type === "formal_parameter") {
          params.push(getNodeText(param, source));
        }
      }
    }
  }
  return params;
}

const FIELD_TYPE_NODES = new Set([
  "type_identifier", "scoped_type_identifier",
  "integral_type", "floating_point_type", "boolean_type",
  "array_type", "generic_type",
]);

function getFieldType(node: TSNode, source: string): string {
  for (const child of node.children) {
    if (FIELD_TYPE_NODES.has(child.type)) {
      return getNodeText(child, source);
    }
  }
  return "";
}

function getFieldValue(declarator: TSNode, source: string): string | undefined {
  for (const child of declarator.children) {
    if (["string_literal", "number_literal", "true", "false"].includes(child.type)) {
      return getNodeText(child, source);
    }
  }
  return undefined;
}

function findDescendantsByType(node: TSNode, typeName: string, out: TSNode[] = []): TSNode[] {
  for (const child of node.children) {
    if (child.type === typeName) {
      out.push(child);
    }
    if (child.children.length > 0) {
      findDescendantsByType(child, typeName, out);
    }
  }
  return out;
}

function getInvokedMethods(node: TSNode, source: string): string[] {
  const names = new Set<string>();
  for (const invocation of findDescendantsByType(node, "method_invocation")) {
    for (const child of invocation.children) {
      if (child.type === "identifier") {
        const name = getNodeText(child, source).trim();
        if (name) {
          names.add(name);
        }
      }
    }
  }
  return [...names];
}
