// Preset & GLB persistence utilities
// - savePreset / loadPreset: download/upload tweak settings as JSON
// - GLB cache: store uploaded GLB files in IndexedDB so they survive reload
// - URL loader: fetch GLB from public URL

const PresetUtils = (() => {

  // ---------- IndexedDB helpers ----------
  // The metaspeak IDB schema is shared with telemetry.jsx. We bump the
  // version whenever any module adds a new store. v3 added the 'events'
  // store; we mirror the same upgrade logic here so that whichever
  // module's openDB() runs first, the DB ends up with all required
  // stores. This avoids VersionError when one module opens v2 after
  // another has already moved the DB to v3.
  const DB_NAME = 'metaspeak';
  const DB_VERSION = 3;
  const STORE_GLB = 'glb';
  const STORE_ANIM = 'anim';
  const STORE_EVENTS = 'events';

  function openDB() {
    return new Promise((resolve, reject) => {
      const req = indexedDB.open(DB_NAME, DB_VERSION);
      req.onupgradeneeded = () => {
        const db = req.result;
        if (!db.objectStoreNames.contains(STORE_GLB)) {
          db.createObjectStore(STORE_GLB);
        }
        if (!db.objectStoreNames.contains(STORE_ANIM)) {
          db.createObjectStore(STORE_ANIM);
        }
        if (!db.objectStoreNames.contains(STORE_EVENTS)) {
          const store = db.createObjectStore(STORE_EVENTS, { keyPath: 'id', autoIncrement: true });
          store.createIndex('ts', 'ts', { unique: false });
          store.createIndex('type', 'type', { unique: false });
        }
      };
      req.onsuccess = () => resolve(req.result);
      req.onerror = () => reject(req.error);
    });
  }

  async function saveGLB(slot, name, arrayBuffer) {
    const db = await openDB();
    return new Promise((resolve, reject) => {
      const tx = db.transaction(STORE_GLB, 'readwrite');
      const store = tx.objectStore(STORE_GLB);
      store.put({ name, buffer: arrayBuffer, savedAt: Date.now() }, slot);
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
    });
  }

  async function loadGLBFromCache(slot) {
    const db = await openDB();
    return new Promise((resolve, reject) => {
      const tx = db.transaction(STORE_GLB, 'readonly');
      const store = tx.objectStore(STORE_GLB);
      const req = store.get(slot);
      req.onsuccess = () => resolve(req.result || null);
      req.onerror = () => reject(req.error);
    });
  }

  async function clearGLBCache(slot) {
    const db = await openDB();
    return new Promise((resolve, reject) => {
      const tx = db.transaction(STORE_GLB, 'readwrite');
      const store = tx.objectStore(STORE_GLB);
      store.delete(slot);
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
    });
  }

  // ---------- Animation cache (per character + per slot) ----------
  // Key shape: `${charSlot}:${animSlot}` e.g. "char1:idle", "char2:talking"
  // Value: { name, buffer, savedAt }
  function animKey(charSlot, animSlot) { return `${charSlot}:${animSlot}`; }

  async function saveAnim(charSlot, animSlot, name, arrayBuffer) {
    const db = await openDB();
    return new Promise((resolve, reject) => {
      const tx = db.transaction(STORE_ANIM, 'readwrite');
      const store = tx.objectStore(STORE_ANIM);
      store.put({ name, buffer: arrayBuffer, savedAt: Date.now() }, animKey(charSlot, animSlot));
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
    });
  }

  async function loadAnimFromCache(charSlot, animSlot) {
    const db = await openDB();
    return new Promise((resolve, reject) => {
      const tx = db.transaction(STORE_ANIM, 'readonly');
      const store = tx.objectStore(STORE_ANIM);
      const req = store.get(animKey(charSlot, animSlot));
      req.onsuccess = () => resolve(req.result || null);
      req.onerror = () => reject(req.error);
    });
  }

  async function listCachedAnims() {
    // Returns array of { key, name, savedAt } for restoration
    const db = await openDB();
    return new Promise((resolve, reject) => {
      const tx = db.transaction(STORE_ANIM, 'readonly');
      const store = tx.objectStore(STORE_ANIM);
      const result = [];
      const cursorReq = store.openCursor();
      cursorReq.onsuccess = () => {
        const cursor = cursorReq.result;
        if (cursor) {
          const v = cursor.value;
          result.push({ key: cursor.key, name: v.name, savedAt: v.savedAt });
          cursor.continue();
        } else {
          resolve(result);
        }
      };
      cursorReq.onerror = () => reject(cursorReq.error);
    });
  }

  async function clearAnimCache(charSlot, animSlot) {
    const db = await openDB();
    return new Promise((resolve, reject) => {
      const tx = db.transaction(STORE_ANIM, 'readwrite');
      const store = tx.objectStore(STORE_ANIM);
      if (charSlot && animSlot) {
        store.delete(animKey(charSlot, animSlot));
      } else {
        store.clear();   // wipe all animations
      }
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
    });
  }

  // ---------- GLB URL loader ----------
  function rewriteUrlForCORS(url) {
    // Dropbox: rewrite "www.dropbox.com" → "dl.dropboxusercontent.com" and force dl=1
    // (dl.dropboxusercontent.com does send CORS headers; www.dropbox.com does not.)
    try {
      const u = new URL(url);
      if (u.hostname === 'www.dropbox.com' || u.hostname === 'dropbox.com') {
        u.hostname = 'dl.dropboxusercontent.com';
        u.searchParams.set('dl', '1');
        return u.toString();
      }
      // Google Drive: convert /file/d/<id>/view to direct download
      if (u.hostname.includes('drive.google.com')) {
        const m = url.match(/\/file\/d\/([^/]+)/);
        if (m) return `https://drive.google.com/uc?export=download&id=${m[1]}`;
      }
      // GitHub blob → raw
      if (u.hostname === 'github.com') {
        const m = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/(.+)$/);
        if (m) return `https://raw.githubusercontent.com/${m[1]}/${m[2]}/${m[3]}`;
      }
      return url;
    } catch {
      return url;
    }
  }

  async function loadGLBFromURL(rawUrl) {
    const url = rewriteUrlForCORS(rawUrl);
    const tryFetch = async (u) => {
      const r = await fetch(u, { mode: 'cors' });
      if (!r.ok) throw new Error(`HTTP ${r.status} ${r.statusText}`);
      return await r.arrayBuffer();
    };
    try {
      return await tryFetch(url);
    } catch (e) {
      // Final fallback: try a public CORS proxy
      try {
        const proxied = `https://corsproxy.io/?${encodeURIComponent(url)}`;
        return await tryFetch(proxied);
      } catch (e2) {
        throw new Error(`Direct: ${e.message}. Proxy fallback also failed: ${e2.message}. Try a different host (raw GitHub, Cloudflare R2, or upload via the file picker instead).`);
      }
    }
  }

  // ---------- Preset save/load ----------
  function downloadPreset(values, filename = 'metaspeak-preset.json') {
    // Strip API keys for safety
    const safe = { ...values };
    delete safe.claudeKey;
    delete safe.elevenlabsKey;
    delete safe.lemonfoxKey;
    // Keep pixabay since it's a public key
    const blob = new Blob([JSON.stringify(safe, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }

  function downloadPresetWithKeys(values, filename = 'metaspeak-preset-with-keys.json') {
    const blob = new Blob([JSON.stringify(values, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }

  async function readPresetFile(file) {
    const text = await file.text();
    return JSON.parse(text);
  }

  // ---------- localStorage backup (smaller, faster) ----------
  // For settings that are too big for the EDITMODE block (causing 413),
  // we keep a localStorage mirror and rehydrate on load.
  const LS_KEY = 'metaspeak-tweaks-mirror';

  function saveToLocalStorage(values) {
    try {
      localStorage.setItem(LS_KEY, JSON.stringify(values));
      return true;
    } catch (e) {
      console.warn('localStorage save failed', e);
      return false;
    }
  }

  function loadFromLocalStorage() {
    try {
      const raw = localStorage.getItem(LS_KEY);
      if (!raw) return null;
      return JSON.parse(raw);
    } catch (e) {
      console.warn('localStorage load failed', e);
      return null;
    }
  }

  function clearLocalStorage() {
    try { localStorage.removeItem(LS_KEY); } catch {}
  }

  return {
    saveGLB, loadGLBFromCache, clearGLBCache,
    saveAnim, loadAnimFromCache, listCachedAnims, clearAnimCache,
    loadGLBFromURL,
    downloadPreset, downloadPresetWithKeys, readPresetFile,
    saveToLocalStorage, loadFromLocalStorage, clearLocalStorage,
  };
})();

window.PresetUtils = PresetUtils;
