/**
 * Java source file parsing using tree-sitter (Node.js bindings).
 *
 * Uses the `tree-sitter` and `tree-sitter-java` npm packages.
 */

import { readFileSync } from "node:fs";
import { ParseError } from "../errors";

// tree-sitter and tree-sitter-java are native modules loaded at runtime.
// They must be listed in package.json and serverExternalPackages in next.config.
// eslint-disable-next-line @typescript-eslint/no-require-imports
let Parser: typeof import("tree-sitter");
// eslint-disable-next-line @typescript-eslint/no-require-imports
let JavaLang: unknown;

let _parser: InstanceType<typeof import("tree-sitter")> | null = null;

function getParser(): InstanceType<typeof import("tree-sitter")> {
  if (_parser) return _parser;

  // Dynamic require — these are native C++ addons that can't be statically imported
  // eslint-disable-next-line @typescript-eslint/no-require-imports
  Parser = require("tree-sitter");
  // eslint-disable-next-line @typescript-eslint/no-require-imports
  JavaLang = require("tree-sitter-java");

  _parser = new Parser();
  _parser.setLanguage(JavaLang as Parameters<typeof _parser.setLanguage>[0]);
  return _parser;
}

export interface TSNode {
  type: string;
  startIndex: number;
  endIndex: number;
  startPosition: { row: number; column: number };
  endPosition: { row: number; column: number };
  text: string;
  children: TSNode[];
  childCount: number;
  hasError: boolean;
}

export interface TSTree {
  rootNode: TSNode;
}

/**
 * Detect whether a buffer is valid UTF-8. If not, it's likely Shift-JIS
 * (common in Japanese enterprise Java codebases).
 */
function isValidUtf8(buf: Buffer): boolean {
  try {
    new TextDecoder("utf-8", { fatal: true }).decode(buf);
    return true;
  } catch {
    return false;
  }
}

/**
 * Decode a buffer to string, auto-detecting encoding.
 * Tries UTF-8 first; falls back to Shift-JIS if invalid.
 */
function decodeSource(buf: Buffer, filePath: string): string {
  if (isValidUtf8(buf)) {
    return buf.toString("utf-8");
  }

  // Try Shift-JIS (Node.js ICU supports this natively)
  try {
    const decoded = new TextDecoder("shift_jis", { fatal: false }).decode(buf);
    console.log(`[scan.parse.encoding] ${filePath}: decoded as Shift-JIS`);
    return decoded;
  } catch {
    // Last resort: UTF-8 with replacement characters
    console.warn(`[scan.parse.encoding] ${filePath}: falling back to lossy UTF-8`);
    return buf.toString("utf-8");
  }
}

/**
 * Maximum source length (in characters) that can be passed directly to
 * tree-sitter's `parse(string)`.  tree-sitter 0.21.x has a ~32 KB
 * character limit on string inputs; beyond that it throws
 * "Invalid argument".  For larger files we use the callback API.
 */
const DIRECT_PARSE_CHAR_LIMIT = 30_000;

/**
 * Chunk size returned by the callback when using the callback-based parse
 * path.  Returning moderate-sized slices keeps the overhead low while
 * avoiding the 32 KB string-input cap.
 */
const CALLBACK_CHUNK_SIZE = 16_384;

export function parseJavaFile(filePath: string): { tree: TSTree; source: string } {
  let sourceBytes: Buffer;
  try {
    sourceBytes = readFileSync(filePath);
  } catch (e) {
    throw new ParseError(
      filePath,
      `Cannot read file: ${e instanceof Error ? e.message : e}`,
    );
  }

  const parser = getParser();
  let source = decodeSource(sourceBytes, filePath);

  // Normalise line endings — CRLF / bare CR → LF.
  // Many enterprise Java files use CRLF; tree-sitter handles LF natively.
  if (source.includes("\r")) {
    source = source.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
  }

  // tree-sitter 0.21.x throws "Invalid argument" when the input string
  // exceeds ~32 K characters.  Use the callback-based API for large files.
  let tree;
  if (source.length <= DIRECT_PARSE_CHAR_LIMIT) {
    tree = parser.parse(source);
  } else {
    tree = parser.parse((index: number) => {
      if (index >= source.length) return "";
      return source.slice(index, index + CALLBACK_CHUNK_SIZE);
    });
  }

  if (tree.rootNode.hasError) {
    console.warn(`[scan.parse.errors] ${filePath}`);
  }

  return { tree: tree as unknown as TSTree, source };
}

/**
 * Force-release the previous tree-sitter native AST memory by parsing
 * a tiny dummy string. tree-sitter internally frees the previous tree
 * when a new parse is started on the same parser instance.
 */
export function releaseTreeMemory(): void {
  const parser = getParser();
  parser.parse("");
}

// tree-sitter Node.js startIndex/endIndex are character offsets (UTF-16 code units),
// so use string.slice() rather than Buffer.subarray().
export function getNodeText(node: TSNode, source: string): string {
  return source.slice(node.startIndex, node.endIndex);
}
