// IndexedDB-backed model cache for transformers.js.
//
// Why? Some browsers (and headless test environments) don't expose the
// Cache API that transformers.js uses for model storage. We pre-download
// the model files once into IndexedDB and serve them back as blob URLs so
// transformers.js doesn't need Cache API at all.
//
// Files we cache (per model):
//   - tokenizer.json
//   - tokenizer_config.json
//   - config.json
//   - generation_config.json
//   - preprocessor_config.json
//   - model_quantized.onnx (the big one — ~40MB tiny, ~75MB base)
//   - vocab.json / merges.txt (English-only models)
//
// Model URLs follow the HuggingFace hub layout:
//   https://huggingface.co/<model_id>/resolve/main/<filename>

const DB_NAME = 'capcut-models';
const DB_VERSION = 1;
const STORE = 'files';

export interface ModelFile {
  url: string;
  blob: Blob;
}

let dbPromise: Promise<IDBDatabase> | null = null;
function openDB(): Promise<IDBDatabase> {
  if (dbPromise) return dbPromise;
  if (typeof indexedDB === 'undefined') {
    return Promise.reject(new Error('IndexedDB ไม่พร้อมใช้งาน'));
  }
  dbPromise = new Promise((resolve, reject) => {
    const req = indexedDB.open(DB_NAME, DB_VERSION);
    req.onupgradeneeded = () => {
      const db = req.result;
      if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE, { keyPath: 'url' });
    };
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
  return dbPromise;
}

async function getFile(url: string): Promise<Blob | null> {
  try {
    const db = await openDB();
    return await new Promise((resolve) => {
      const tx = db.transaction(STORE, 'readonly');
      const req = tx.objectStore(STORE).get(url);
      req.onsuccess = () => resolve(req.result?.blob ?? null);
      req.onerror = () => resolve(null);
    });
  } catch {
    return null;
  }
}

async function putFile(url: string, blob: Blob): Promise<void> {
  try {
    const db = await openDB();
    await new Promise<void>((resolve, reject) => {
      const tx = db.transaction(STORE, 'readwrite');
      tx.objectStore(STORE).put({ url, blob });
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
    });
  } catch {
    /* ignore */
  }
}

const HF_BASE = 'https://huggingface.co';

/** Common model file names. transformers.js will sometimes pull more — we
 * list the known ones so we can pre-warm deterministically. */
const COMMON_FILES = [
  'tokenizer.json',
  'tokenizer_config.json',
  'config.json',
  'generation_config.json',
  'preprocessor_config.json',
  'normalizer.json',
  'vocab.json',
  'merges.txt',
  'added_tokens.json',
  'special_tokens_map.json',
  'model_quantized.onnx',
];

interface EnsureResult {
  cached: number;
  total: number;
  totalBytes: number;
  /** Files transformers.js asked for that weren't in our prewarm list — we
   *  fetch those on demand so the pipeline still works. */
  onDemand: number;
}

/** Pre-download all known files for a model into IndexedDB. Also fetches
 * any extra file transformers.js asks for if it isn't already cached. */
export async function ensureWhisperModel(
  modelId = 'Xenova/whisper-tiny',
  onProgress?: (file: string, index: number, total: number) => void,
): Promise<EnsureResult> {
  const urlFor = (file: string) => `${HF_BASE}/${modelId}/resolve/main/${file}`;

  // First pass: prewarm the common files.
  let cached = 0;
  let totalBytes = 0;
  for (let i = 0; i < COMMON_FILES.length; i++) {
    const filename = COMMON_FILES[i];
    const url = urlFor(filename);
    onProgress?.(filename, i, COMMON_FILES.length);
    const existing = await getFile(url);
    if (existing) {
      cached++;
      totalBytes += existing.size;
      continue;
    }
    try {
      const resp = await fetch(url);
      if (!resp.ok) {
        // 404 is fine — not all models have every file.
        if (resp.status !== 404) {
          const blob = await resp.blob();
          await putFile(url, blob);
          cached++;
          totalBytes += blob.size;
        }
        continue;
      }
      const blob = await resp.blob();
      await putFile(url, blob);
      cached++;
      totalBytes += blob.size;
    } catch {
      /* ignore individual failures */
    }
  }
  return { cached, total: COMMON_FILES.length, totalBytes, onDemand: 0 };
}

/** Called by the fetch interceptor when transformers.js asks for a file
 * that's not yet in cache. */
export async function fetchAndCache(modelId: string, file: string): Promise<Blob | null> {
  const url = `${HF_BASE}/${modelId}/resolve/main/${file}`;
  try {
    const resp = await fetch(url);
    if (!resp.ok) return null;
    const blob = await resp.blob();
    await putFile(url, blob);
    return blob;
  } catch {
    return null;
  }
}

/**
 * Patch transformers.js' RemoteHub so that requests for our cached model
 * files resolve to blob URLs from IndexedDB. If a file isn't cached yet
 * (e.g. new model just released) we fetch + cache it on demand.
 */
export function installCacheInterception(): void {
  if ((window as unknown as { __patchedFetch?: boolean }).__patchedFetch) return;
  const originalFetch = window.fetch.bind(window);
  (window as unknown as { __originalFetch: typeof fetch }).__originalFetch = originalFetch;

  window.fetch = async function patchedFetch(
    input: RequestInfo | URL,
    init?: RequestInit,
  ): Promise<Response> {
    const url =
      typeof input === 'string'
        ? input
        : input instanceof URL
          ? input.toString()
          : input.url;
    // Match any Xenova whisper model.
    const match = url.match(/huggingface\.co\/(Xenova\/whisper-[^/]+)\/resolve\/main\/(.+?)(?:\?|$)/);
    if (match) {
      const [, modelId, filename] = match;
      try {
        const cached = await getFile(url);
        if (cached) {
          return new Response(cached, {
            status: 200,
            statusText: 'OK (from IndexedDB cache)',
            headers: { 'Content-Type': 'application/octet-stream' },
          });
        }
        // Cache miss → fetch + cache + serve.
        const fresh = await fetchAndCache(modelId, filename);
        if (fresh) {
          return new Response(fresh, {
            status: 200,
            headers: { 'Content-Type': 'application/octet-stream' },
          });
        }
      } catch {
        /* fall through to network */
      }
    }
    return originalFetch(input, init);
  };
  (window as unknown as { __patchedFetch: boolean }).__patchedFetch = true;
}

/** Pre-warm: ensure the cache is populated BEFORE transformers is invoked. */
export async function prewarmWhisperModel(
  onProgress?: (file: string, pct: number) => void,
): Promise<void> {
  const result = await ensureWhisperModel('Xenova/whisper-tiny', (file, i, total) => {
    onProgress?.(file, Math.round((i / total) * 100));
  });
  onProgress?.('done', 100);
  void result;
}