/**
 * Regex-based fallback extractor for Java files.
 *
 * Used when tree-sitter fails to parse a file (e.g. encoding issues,
 * very large files, or unusual syntax). Extracts enough structure for
 * the knowledge graph to include in module evidence and wiki generation.
 *
 * NOT a replacement for tree-sitter — just good enough for:
 * - Package declaration
 * - Class/interface/enum names with extends/implements
 * - Method signatures with line numbers
 * - Static final field declarations
 */

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

/** Regex for `package some.thing;` */
const PACKAGE_RE = /^\s*package\s+([\w.]+)\s*;/m;

/** Regex for class/interface/enum declarations */
const TYPE_DECL_RE =
  /^[ \t]*(?:(?:public|private|protected)\s+)?(?:(?:abstract|static|final)\s+)*(?:(class|interface|enum))\s+(\w+)(?:\s+extends\s+([\w.]+))?(?:\s+implements\s+([\w.,\s]+))?/gm;

/** Regex for method declarations (covers common Java method signatures) */
const METHOD_RE =
  /^[ \t]*(?:(?:public|private|protected)\s+)?(?:(?:static|final|abstract|synchronized|native)\s+)*(?:(?:<[\w<>,?\s]+>\s+)?)?([\w<>\[\].?]+)\s+(\w+)\s*\(([^)]*)\)\s*(?:throws\s+[\w.,\s]+)?\s*[{;]/gm;

/** Regex for static final fields (constants) */
const CONSTANT_RE =
  /^[ \t]*(?:public|private|protected)\s+static\s+final\s+([\w<>\[\]]+)\s+(\w+)\s*=\s*(.+?)\s*;/gm;

/** Regex for field declarations */
const FIELD_RE =
  /^[ \t]*(?:(?:public|private|protected)\s+)?(?:(?:static|final|volatile|transient)\s+)*([\w<>\[\].?]+)\s+(\w+)\s*[=;]/gm;

/** Regex for import declarations */
const IMPORT_RE = /^\s*import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;/gm;

interface RegexExtractionResult {
  nodes: GraphNode[];
  edges: GraphEdge[];
  classCount: number;
  methodCount: number;
}

export function regexExtractFromFile(
  source: string,
  filePath: string,
): RegexExtractionResult {
  const nodes: GraphNode[] = [];
  const edges: GraphEdge[] = [];
  const lines = source.split("\n");

  // Package
  const pkgMatch = PACKAGE_RE.exec(source);
  const pkg = pkgMatch ? pkgMatch[1] : "";

  // File node
  const fileNode = makeFileNode(filePath, pkg, lines.length);
  fileNode.attributes.extraction_method = "regex_fallback";
  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: {},
    });
  }

  // Imports
  for (const match of source.matchAll(IMPORT_RE)) {
    edges.push({
      source: fileNode.id,
      target: `import:${match[1]}`,
      type: EdgeType.IMPORTS,
      attributes: {},
    });
  }

  // Build a line-number lookup (character offset → line number)
  const lineOffsets: number[] = [0];
  for (let i = 0; i < source.length; i++) {
    if (source[i] === "\n") {
      lineOffsets.push(i + 1);
    }
  }
  function offsetToLine(offset: number): number {
    let lo = 0;
    let hi = lineOffsets.length - 1;
    while (lo < hi) {
      const mid = (lo + hi + 1) >> 1;
      if (lineOffsets[mid] <= offset) lo = mid;
      else hi = mid - 1;
    }
    return lo + 1; // 1-based
  }

  // Type declarations (class/interface/enum)
  let classCount = 0;
  const classIds = new Map<string, string>(); // className → nodeId

  for (const match of source.matchAll(TYPE_DECL_RE)) {
    const kind = match[1]; // "class" | "interface" | "enum"
    const name = match[2];
    const extendsName = match[3] || "";
    const implementsRaw = match[4] || "";
    const implementsNames = implementsRaw
      .split(",")
      .map((s) => s.trim())
      .filter(Boolean);

    const lineNum = offsetToLine(match.index!);
    const nodeType =
      kind === "interface"
        ? NodeType.INTERFACE
        : kind === "enum"
          ? NodeType.ENUM
          : NodeType.CLASS;

    const classId = makeNodeId(filePath, name, nodeType);
    classIds.set(name, classId);

    // Estimate end line: find matching closing brace (rough)
    const endLine = estimateBlockEnd(lines, lineNum - 1);

    const location: SourceLocation = {
      filePath,
      lineStart: lineNum,
      lineEnd: endLine,
    };

    const attrs: Record<string, unknown> = {
      extraction_method: "regex_fallback",
    };
    if (extendsName) attrs.extends = extendsName;
    if (implementsNames.length > 0) attrs.implements = implementsNames;

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

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

    classCount++;
  }

  // If no classes found, create a synthetic class from the file name
  if (classCount === 0) {
    const baseName = filePath.split("/").pop()?.replace(".java", "") || "Unknown";
    const classId = makeNodeId(filePath, baseName, NodeType.CLASS);
    classIds.set(baseName, classId);

    nodes.push({
      id: classId,
      type: NodeType.CLASS,
      name: baseName,
      location: { filePath, lineStart: 1, lineEnd: lines.length },
      attributes: { extraction_method: "regex_fallback", synthetic: true },
    });
    edges.push({ source: fileNode.id, target: classId, type: EdgeType.CONTAINS, attributes: {} });
    classCount = 1;
  }

  // Methods — assign to the most likely parent class
  let methodCount = 0;
  const seenMethods = new Set<string>();

  for (const match of source.matchAll(METHOD_RE)) {
    const returnType = match[1];
    const methodName = match[2];
    const params = match[3];

    // Skip constructor-like (return type matches a known class name)
    if (classIds.has(returnType)) continue;
    // Skip common false positives
    if (["if", "for", "while", "switch", "catch", "return", "new", "throw", "else"].includes(methodName)) continue;

    const lineNum = offsetToLine(match.index!);
    const dedupKey = `${methodName}:${lineNum}`;
    if (seenMethods.has(dedupKey)) continue;
    seenMethods.add(dedupKey);

    // Find parent class: the class whose line range contains this method
    const parentClassId = findParentClass(classIds, nodes, lineNum) || [...classIds.values()][0];
    if (!parentClassId) continue;

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

    // Don't create duplicate method nodes (overloads get same id)
    if (seenMethods.has(methodId)) {
      // Append param hint to disambiguate
      const uniqueMethodId = makeNodeId(filePath, `${className}.${methodName}_L${lineNum}`, NodeType.METHOD);
      const endLine = estimateMethodEnd(lines, lineNum - 1);
      const paramList = params
        .split(",")
        .map((p) => p.trim())
        .filter(Boolean);

      nodes.push({
        id: uniqueMethodId,
        type: NodeType.METHOD,
        name: methodName,
        location: { filePath, lineStart: lineNum, lineEnd: endLine },
        attributes: {
          return_type: returnType,
          parameters: paramList,
          extraction_method: "regex_fallback",
        },
      });
      edges.push({ source: parentClassId, target: uniqueMethodId, type: EdgeType.CONTAINS, attributes: {} });
    } else {
      seenMethods.add(methodId);
      const endLine = estimateMethodEnd(lines, lineNum - 1);
      const paramList = params
        .split(",")
        .map((p) => p.trim())
        .filter(Boolean);

      nodes.push({
        id: methodId,
        type: NodeType.METHOD,
        name: methodName,
        location: { filePath, lineStart: lineNum, lineEnd: endLine },
        attributes: {
          return_type: returnType,
          parameters: paramList,
          extraction_method: "regex_fallback",
        },
      });
      edges.push({ source: parentClassId, target: methodId, type: EdgeType.CONTAINS, attributes: {} });
    }

    methodCount++;
  }

  // Constants (static final fields)
  for (const match of source.matchAll(CONSTANT_RE)) {
    const fieldType = match[1];
    const fieldName = match[2];
    const value = match[3].trim();
    const lineNum = offsetToLine(match.index!);

    const parentClassId = findParentClass(classIds, nodes, lineNum) || [...classIds.values()][0];
    if (!parentClassId) continue;

    const className = parentClassId.split(":").pop() || "";
    const fieldId = makeNodeId(filePath, `${className}.${fieldName}`, NodeType.FIELD);

    nodes.push({
      id: fieldId,
      type: NodeType.FIELD,
      name: fieldName,
      location: { filePath, lineStart: lineNum, lineEnd: lineNum },
      attributes: {
        modifiers: ["static", "final"],
        field_type: fieldType,
        value,
        extraction_method: "regex_fallback",
      },
    });
    edges.push({ source: parentClassId, target: fieldId, type: EdgeType.CONTAINS, attributes: {} });
  }

  console.log(
    `[scan.regex_fallback] ${filePath}: ${classCount} classes, ${methodCount} methods`,
  );

  return { nodes, edges, classCount, methodCount };
}

/**
 * Find which class contains a given line number.
 */
function findParentClass(
  classIds: Map<string, string>,
  nodes: GraphNode[],
  lineNum: number,
): string | undefined {
  let bestId: string | undefined;
  let bestStart = 0;

  for (const [, classId] of classIds) {
    const node = nodes.find((n) => n.id === classId);
    if (!node?.location) continue;
    if (lineNum >= node.location.lineStart && lineNum <= node.location.lineEnd) {
      // Prefer the innermost (most recently started) class
      if (node.location.lineStart > bestStart) {
        bestStart = node.location.lineStart;
        bestId = classId;
      }
    }
  }

  return bestId;
}

/**
 * Estimate where a class body ends by tracking brace depth from a given line.
 */
function estimateBlockEnd(lines: string[], startLineIdx: number): number {
  let depth = 0;
  let foundOpen = false;

  for (let i = startLineIdx; i < lines.length; i++) {
    const line = lines[i];
    // Skip strings (very rough — won't handle all edge cases)
    const stripped = line.replace(/"(?:[^"\\]|\\.)*"/g, "").replace(/'(?:[^'\\]|\\.)*'/g, "");
    for (const ch of stripped) {
      if (ch === "{") {
        depth++;
        foundOpen = true;
      } else if (ch === "}") {
        depth--;
        if (foundOpen && depth === 0) {
          return i + 1; // 1-based
        }
      }
    }
  }

  return lines.length;
}

/**
 * Estimate where a method body ends. Similar to estimateBlockEnd but
 * starts from the method declaration line.
 */
function estimateMethodEnd(lines: string[], startLineIdx: number): number {
  let depth = 0;
  let foundOpen = false;

  // Scan the entire remaining file — large methods (e.g. 2,900 LOC) must be handled
  for (let i = startLineIdx; i < lines.length; i++) {
    const line = lines[i];
    const stripped = line.replace(/"(?:[^"\\]|\\.)*"/g, "").replace(/'(?:[^'\\]|\\.)*'/g, "");
    for (const ch of stripped) {
      if (ch === "{") {
        depth++;
        foundOpen = true;
      } else if (ch === "}") {
        depth--;
        if (foundOpen && depth === 0) {
          return i + 1;
        }
      }
    }
  }

  return lines.length;
}
