// WEB-ONLY transcription engine, backed by transformers.js (@huggingface/
// transformers) running Whisper in the browser (WebGPU when available, else
// multi-threaded WASM).
//
// 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). The browser resolves it natively. This keeps the JS bundle
// small and is the standard way to run transformers.js under Metro/Expo web.
//
// NOTE: the page must be cross-origin isolated (COOP + COEP) for multi-threaded
// WASM; we use COEP: credentialless so the CDN script and the Hugging Face model
// files (CORS-enabled) load without requiring CORP headers. See docker/nginx.conf.
//
// This module is web-only and is NEVER imported by any vitest test.

import { MODELS, recommendModel } from '../models/catalog';
import type { Backend, ModelId, PcmAudio, Segment, TranscribeOptions } from '../types';
import type { EngineCapabilities, TranscriptionEngine } from './engine';

// Pin the transformers.js version we load at runtime.
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 AsrChunk {
  timestamp: [number, number | null];
  text: string;
}
interface AsrOutput {
  text: string;
  chunks?: AsrChunk[];
}
type AsrPipeline = (audio: Float32Array, opts: Record<string, unknown>) => Promise<AsrOutput>;
interface PipelineOptions {
  device?: string;
  dtype?: string;
  progress_callback?: (e: { status?: string; progress?: number }) => void;
}
interface TransformersModule {
  pipeline: (task: string, model: string, opts?: PipelineOptions) => Promise<AsrPipeline>;
  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;
}

/** Loaded ASR pipelines, keyed by model id. */
const loaded = new Map<ModelId, AsrPipeline>();
let cachedBackend: Backend | undefined;

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

async function resolveBackend(): Promise<Backend> {
  if (cachedBackend) return cachedBackend;
  cachedBackend = (await detectWebGpu()) ? 'webgpu' : 'wasm';
  return cachedBackend;
}

export const engine: TranscriptionEngine = {
  platform: 'web',

  async capabilities(): Promise<EngineCapabilities> {
    const backend = await resolveBackend();
    return {
      backend,
      supportsLiveMic: false,
      maxRecommendedModel: recommendModel({ backend }),
    };
  },

  async loadModel(modelId: ModelId, onProgress?: (p: number) => void): Promise<void> {
    if (loaded.has(modelId)) return;
    const { pipeline } = await lib();
    const webgpu = (await resolveBackend()) === 'webgpu';
    const asr = await pipeline('automatic-speech-recognition', MODELS[modelId].webRepo, {
      // 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);
      },
    });
    loaded.set(modelId, asr);
  },

  isModelLoaded(modelId: ModelId): boolean {
    return loaded.has(modelId);
  },

  async transcribeChunk(audio: PcmAudio, opts: TranscribeOptions): Promise<Segment[]> {
    const asr = loaded.get(opts.modelId);
    if (!asr) throw new Error(`Model "${opts.modelId}" is not loaded; call loadModel() first.`);

    const out = await asr(audio.samples, {
      return_timestamps: true,
      // One window at a time; 30s matches Whisper's frame so it won't re-chunk.
      chunk_length_s: 30,
      language: opts.language,
      task: opts.translate ? 'translate' : 'transcribe',
    });

    const segments: Segment[] = [];
    for (const c of out.chunks ?? []) {
      const [start, end] = c.timestamp;
      if (start == null) continue;
      const text = c.text.trim();
      if (text.length === 0) continue;
      segments.push({ start, end: end ?? start, text });
    }
    return segments;
  },
};
