// NATIVE-ONLY transcription engine, backed by whisper.rn (React Native binding
// of whisper.cpp). This module imports a native module (whisper.rn) and Expo's
// file system, so it is NEVER imported by any vitest test (only the mock-based
// pipeline.test.ts is run). It is selected at runtime by platform-aware code.
//
// whisper.rn API used here (v0.6.0, verified against
// node_modules/whisper.rn/lib/typescript/index.d.ts +
// .../NativeRNWhisper.d.ts and the transcribeData source in src/index.ts):
//
//   initWhisper({ filePath }): Promise<WhisperContext>
//     - `filePath` is a string path to a ggml .bin model file.
//
//   WhisperContext.transcribeData(
//     data: string | ArrayBuffer,        // base64 float32 PCM, or raw ArrayBuffer
//     options?: TranscribeFileOptions,   // { language?, translate?, ... }
//   ): { stop: () => Promise<void>; promise: Promise<TranscribeResult> }
//     - We pass the Float32Array's ArrayBuffer directly (the binding accepts an
//       ArrayBuffer of raw float32 PCM; see transcribeData in src/index.ts).
//     - It returns an object with a `promise`, NOT a bare promise.
//
//   TranscribeResult = {
//     result: string; language: string; isAborted: boolean;
//     segments: Array<{ text: string; t0: number; t1: number }>;
//   }
//     - t0/t1 are whisper.cpp timestamps in CENTISECONDS (1/100 s), so we divide
//       by 100 to get seconds. They are chunk-local because we hand the context
//       one window at a time.

import { initWhisper } from 'whisper.rn';
import type { WhisperContext } from 'whisper.rn';
import { Paths, File } from 'expo-file-system';
import type {
  ModelId,
  PcmAudio,
  Segment,
  TranscribeOptions,
} from '../types';
import { MODELS, recommendModel } from '../models/catalog';
import type { EngineCapabilities, TranscriptionEngine } from './engine';

/** Loaded whisper.cpp contexts, keyed by model id. */
const loaded = new Map<ModelId, WhisperContext>();

/**
 * Resolve the on-device path of a model's ggml file.
 *
 * Models live under `<documentDirectory>/models/<ggml-filename>`. The document
 * directory survives app restarts and OS storage pressure, which is what we want
 * for multi-hundred-MB model files.
 *
 * TODO: the model DOWNLOAD UI / fetcher is NOT built yet. This function only
 *       computes where the file *should* be; the .bin must already exist there
 *       (e.g. side-loaded during development) or loadModel() will reject. Wire a
 *       downloader (File.downloadFileAsync into Paths.document/'models') before
 *       shipping.
 *
 * whisper.rn's initWhisper expects a plain filesystem path. Expo's File `.uri`
 * is a `file://` URI, so we strip the scheme.
 */
function modelPathFor(id: ModelId): string {
  const file = new File(Paths.document, 'models', MODELS[id].ggml);
  // e.g. 'file:///data/.../Documents/models/ggml-tiny.en.bin' -> '/data/.../ggml-tiny.en.bin'
  return file.uri.replace(/^file:\/\//, '');
}

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

  async capabilities(): Promise<EngineCapabilities> {
    // CoreML (iOS Neural Engine) and Metal acceleration are decided at the
    // native build/runtime level (e.g. ContextOptions.useGpu / useCoreMLIos),
    // not something we can reliably probe from JS here. Default to the
    // conservative 'cpu' backend for model gating; an accelerated build still
    // runs fine, it just won't be recommended a larger model by this heuristic.
    const backend = 'cpu' as const;
    return {
      backend,
      // whisper.rn supports realtime/live-mic transcription on device.
      supportsLiveMic: true,
      maxRecommendedModel: recommendModel({ backend }),
    };
  },

  async loadModel(modelId: ModelId): Promise<void> {
    if (loaded.has(modelId)) return; // idempotent

    const filePath = modelPathFor(modelId);
    // initWhisper rejects if the file is missing/corrupt. See the TODO in
    // modelPathFor: until the download UI exists, the .bin must already be there.
    const ctx = await initWhisper({ filePath });
    loaded.set(modelId, ctx);
  },

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

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

    // Pass the raw float32 PCM as an ArrayBuffer. We hand over a tight copy of
    // exactly this chunk's samples: `audio.samples` may be a subarray VIEW into
    // a larger buffer (the pipeline slices with subarray), so `.buffer` alone
    // would expose the whole backing buffer. `.slice()` yields a standalone
    // Float32Array whose `.buffer` is exactly these samples.
    const pcm = audio.samples.slice();

    // transcribeData returns { stop, promise }; await the promise.
    const { promise } = ctx.transcribeData(pcm.buffer, {
      // language undefined => whisper.rn auto-detects ('auto').
      language: opts.language,
      translate: opts.translate ?? false,
    });
    const result = await promise;

    // Map whisper.cpp segments (centiseconds) -> our Segment[] (chunk-local s).
    const segments: Segment[] = [];
    for (const s of result.segments) {
      const text = s.text.trim();
      if (text.length === 0) continue;
      segments.push({
        start: s.t0 / 100, // centiseconds -> seconds
        end: s.t1 / 100,
        text,
      });
    }
    return segments;
  },
};
