import { createRequire } from 'node:module';

import type * as NativeContextUtils from '@octocodeai/octocode-engine';

export type {
  BinaryInspectInfo,
  BinaryStrings,
  ExtractMatchingLinesOptions,
  ExtractMatchingLinesResult,
  FilterPatchOptions,
  FileSystemEntry,
  FileSystemQueryOptions,
  FileSystemQueryResult,
  MinifyResult,
  JsonInput,
  RipgrepParseOptions,
  RipgrepParseResult,
  RipgrepSearchOptions,
  SliceContentOptions,
  SliceContentResult,
  StructuralMatch,
  YamlConversionConfig,
} from '@octocodeai/octocode-engine';

type NativeContextUtilsModule = typeof NativeContextUtils;
type NativeLoader = () => NativeContextUtilsModule;

const require = createRequire(import.meta.url);
const NATIVE_PACKAGE_NAME = '@octocodeai/octocode-engine';

let cachedNative: NativeContextUtilsModule | undefined;
let nativeLoader: NativeLoader = () =>
  require(NATIVE_PACKAGE_NAME) as NativeContextUtilsModule;

export class ContextUtilsLoadError extends Error {
  constructor(readonly cause: unknown) {
    super(`Failed to load native dependency ${NATIVE_PACKAGE_NAME}`);
    this.name = 'ContextUtilsLoadError';
  }
}

function loadNative(): NativeContextUtilsModule {
  if (cachedNative) return cachedNative;

  try {
    cachedNative = nativeLoader();
    return cachedNative;
  } catch (error) {
    throw new ContextUtilsLoadError(error);
  }
}

export function setContextUtilsNativeLoaderForTesting(
  loader: NativeLoader
): void {
  nativeLoader = loader;
  cachedNative = undefined;
}

export function resetContextUtilsNativeLoaderForTesting(): void {
  nativeLoader = () => require(NATIVE_PACKAGE_NAME) as NativeContextUtilsModule;
  cachedNative = undefined;
}

export const contextUtils = {
  applyContentViewMinification(content: string, filePath: string): string {
    return loadNative().applyContentViewMinification(content, filePath);
  },

  applyMinification(content: string, filePath: string): string {
    return loadNative().applyMinification(content, filePath);
  },

  minifyContent(
    content: string,
    filePath: string
  ): Promise<NativeContextUtils.MinifyResult> {
    return loadNative().minifyContent(content, filePath);
  },

  minifyContentSync(content: string, filePath: string): string {
    return loadNative().minifyContentSync(content, filePath);
  },

  minifyContentResult(
    content: string,
    filePath: string
  ): NativeContextUtils.MinifyResult {
    return loadNative().minifyContentResult(content, filePath);
  },

  minifyMarkdownCore(content: string): string {
    return loadNative().minifyMarkdownCore(content);
  },

  extractSignatures(content: string, filePath: string): string | null {
    return loadNative().extractSignatures(content, filePath);
  },

  /**
   * Native JS/TS document symbols as a JSON `DocumentSymbol[]` string, or null
   * when oxc declines the input (non-JS/TS, oversized, hard parse failure, or
   * no symbols). Server-free, syntax-only — no type inference.
   */
  extractJsSymbols(content: string, filePath: string): string | null {
    return loadNative().extractJsSymbols(content, filePath);
  },

  /**
   * Native in-file references as a JSON `Range[]` (declaration first), or null
   * when oxc declines the input or the cursor is not on a resolvable binding.
   * Same-file only, syntax-only — no type inference, no cross-file resolution.
   */
  findInFileReferences(
    content: string,
    filePath: string,
    line: number,
    character: number
  ): string | null {
    return loadNative().findInFileReferences(
      content,
      filePath,
      line,
      character
    );
  },

  /**
   * Canonical lowercase extensions (no leading dot) the native oxc JS/TS path
   * handles. Source of truth lives in the engine — gate native dispatch on this.
   */
  getSupportedJsTsExtensions(): string[] {
    return loadNative().getSupportedJsTsExtensions();
  },

  structuralSearch(
    content: string,
    filePath: string,
    pattern?: string | null,
    rule?: string | null
  ): NativeContextUtils.StructuralMatch[] {
    return loadNative().structuralSearch(content, filePath, pattern, rule);
  },

  structuralSearchFiles(
    options: NativeContextUtils.StructuralSearchFilesOptions
  ): NativeContextUtils.StructuralSearchFilesResult {
    return loadNative().structuralSearchFiles(options);
  },

  getSupportedStructuralExtensions(): string[] {
    return loadNative().getSupportedStructuralExtensions();
  },

  /**
   * Native binary inspection (format lane). Parses an executable / object /
   * archive and returns identity + symbols/imports/exports/sections/deps.
   * Degrades to magic-byte identity on malformed input; only unreadable or
   * oversized files throw.
   */
  inspectBinaryNative(path: string): NativeContextUtils.BinaryInspectInfo {
    return loadNative().inspectBinaryNative(path);
  },

  /**
   * Native strings extraction — printable ASCII + UTF-16 (LE/BE) runs of at
   * least `minLength`, longest-first, optionally hex offset-prefixed.
   */
  extractBinaryStringsNative(
    path: string,
    minLength: number,
    includeOffsets: boolean,
    scanOffset = 0
  ): NativeContextUtils.BinaryStrings {
    return loadNative().extractBinaryStringsNative(
      path,
      minLength,
      includeOffsets,
      scanOffset
    );
  },

  validateRipgrepPattern(
    pattern: string,
    fixedString?: boolean | null,
    perlRegex?: boolean | null
  ): NativeContextUtils.RipgrepPatternValidationResult {
    return loadNative().validateRipgrepPattern(pattern, fixedString, perlRegex);
  },

  getSemanticBoundaryOffsets(content: string, filePath: string): number[] {
    return loadNative().getSemanticBoundaryOffsets(content, filePath);
  },

  jsonToYamlString(
    jsonObject: NativeContextUtils.JsonInput,
    config?: NativeContextUtils.YamlConversionConfig | null
  ): string {
    return loadNative().jsonToYamlString(jsonObject, config);
  },

  parseRipgrepJson(
    stdout: string,
    options?: NativeContextUtils.RipgrepParseOptions | null
  ): NativeContextUtils.RipgrepParseResult {
    return loadNative().parseRipgrepJson(stdout, options);
  },

  searchRipgrep(
    options: NativeContextUtils.RipgrepSearchOptions
  ): Promise<NativeContextUtils.RipgrepParseResult> {
    return loadNative().searchRipgrep(options);
  },

  queryFileSystem(
    options: NativeContextUtils.FileSystemQueryOptions
  ): NativeContextUtils.FileSystemQueryResult {
    return loadNative().queryFileSystem(options);
  },

  extractMatchingLines(
    content: string,
    pattern: string,
    options?: NativeContextUtils.ExtractMatchingLinesOptions | null
  ): NativeContextUtils.ExtractMatchingLinesResult {
    return loadNative().extractMatchingLines(content, pattern, options);
  },

  filterPatch(
    patch: string,
    options?: NativeContextUtils.FilterPatchOptions | null
  ): string {
    return loadNative().filterPatch(patch, options);
  },

  charToByteOffset(content: string, charIndex: number): number {
    return loadNative().charToByteOffset(content, charIndex);
  },

  byteToCharOffset(content: string, byteOffset: number): number {
    return loadNative().byteToCharOffset(content, byteOffset);
  },

  byteSliceContent(
    content: string,
    byteStart: number,
    byteEnd: number
  ): string {
    return loadNative().byteSliceContent(content, byteStart, byteEnd);
  },

  sliceContent(
    content: string,
    charOffset: number,
    charLength: number,
    options?: NativeContextUtils.SliceContentOptions | null
  ): NativeContextUtils.SliceContentResult {
    return loadNative().sliceContent(content, charOffset, charLength, options);
  },

  get SIGNATURES_ONLY_HINT(): string {
    return loadNative().SIGNATURES_ONLY_HINT;
  },
};
