// WEB-ONLY embedding engine, backed by transformers.js (@huggingface/
// transformers) running the multilingual-e5-small feature-extraction model in
// the browser (WebGPU when available, else WASM). Produces unit-normalized,
// mean-pooled 384-d vectors for on-device semantic search (Phase 1).
//
// WHY WE LOAD IT FROM A CDN AT RUNTIME (not a static import):
// transformers.js depends on onnxruntime-web, which uses a *computed* dynamic
// import (`import(/*webpackIgnore*/ a)`) and ships WASM — Metro (Expo's web
// bundler) cannot statically bundle either and fails the build. So we never let
// Metro see the package: we load the ESM build from a CDN at runtime via a
// dynamic import hidden behind `new Function` (so Metro's static analyzer can't
// trip over it). This mirrors src/lib/transcription/engineImpl.web.ts exactly.
//
// NOTE: the page must be cross-origin isolated (COOP + COEP) for multi-threaded
// WASM; see docker/nginx.conf. This module is web-only and is NEVER imported by
// any vitest test.

import type { EmbeddingEngine } from './engine';
import { EMBED_DIM, EMBED_MODEL } from './engine';

// Pin the transformers.js version we load at runtime (matches the ASR engine).
const TRANSFORMERS_CDN = 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0';

// `new Function` hides the dynamic import() specifier from Metro's bundler so it
// never tries to resolve/transform transformers.js or onnxruntime-web.
const runtimeImport = new Function('u', 'return import(u)') as (u: string) => Promise<TransformersModule>;

// Minimal structural types for the bits of transformers.js we use.
interface FeatureTensor {
  /** Nested arrays: one row of floats per input text. */
  tolist(): number[][];
}
type FeatureExtractor = (
  texts: string[],
  opts: { pooling: 'mean'; normalize: boolean },
) => Promise<FeatureTensor>;
interface PipelineOptions {
  device?: string;
  dtype?: string;
  progress_callback?: (e: { status?: string; progress?: number }) => void;
}
interface TransformersModule {
  pipeline: (task: string, model: string, opts?: PipelineOptions) => Promise<FeatureExtractor>;
  env: { allowLocalModels: boolean };
}

let libPromise: Promise<TransformersModule> | null = null;
async function lib(): Promise<TransformersModule> {
  if (!libPromise) {
    libPromise = runtimeImport(TRANSFORMERS_CDN).then((m) => {
      // Never read models off the local filesystem in the browser.
      m.env.allowLocalModels = false;
      return m;
    });
  }
  return libPromise;
}

/** The single cached feature-extraction pipeline (one model). */
let extractor: FeatureExtractor | null = null;
let cachedWebGpu: boolean | undefined;

async function detectWebGpu(): Promise<boolean> {
  if (cachedWebGpu !== undefined) return cachedWebGpu;
  try {
    if (typeof navigator === 'undefined' || !('gpu' in navigator)) {
      cachedWebGpu = false;
      return false;
    }
    const gpu = (navigator as { gpu?: { requestAdapter(): Promise<unknown> } }).gpu;
    const adapter = await gpu?.requestAdapter();
    cachedWebGpu = adapter != null;
  } catch {
    cachedWebGpu = false;
  }
  return cachedWebGpu;
}

export const engine: EmbeddingEngine = {
  platform: 'web',
  dim: EMBED_DIM,

  isLoaded(): boolean {
    return extractor != null;
  },

  async loadModel(onProgress?: (p: number) => void): Promise<void> {
    if (extractor != null) return; // idempotent
    const { pipeline } = await lib();
    const webgpu = await detectWebGpu();
    extractor = await pipeline('feature-extraction', EMBED_MODEL, {
      // WebGPU + fp16 when available; otherwise 8-bit weights on WASM, which
      // stays small to download and runs acceptably on a plain CPU.
      device: webgpu ? 'webgpu' : 'wasm',
      dtype: webgpu ? 'fp16' : 'q8',
      progress_callback: (e) => {
        if (e.status === 'progress' && e.progress != null) onProgress?.(e.progress / 100);
      },
    });
  },

  async embed(texts: string[], kind: 'query' | 'passage'): Promise<Float32Array[]> {
    if (extractor == null) {
      throw new Error('Embedding model is not loaded; call loadModel() first.');
    }
    if (texts.length === 0) return [];
    // e5 asymmetric convention: prefix with "query: " or "passage: ".
    const prefix = `${kind}: `;
    const prefixed = texts.map((t) => prefix + t);
    const tensor = await extractor(prefixed, { pooling: 'mean', normalize: true });
    // tolist() yields one number[] row per input; convert each to Float32Array.
    return tensor.tolist().map((row) => Float32Array.from(row));
  },
};
