// StellarCaptions — IndexedDB for local transcript storage

const DB_NAME = 'StellarCaptions'
const DB_VERSION = 2
const STORE_NAME = 'transcripts'
const SESSION_STORE = 'sessions'

// ---------- Local at-rest encryption (AES-256-GCM) ----------
// Transcripts never leave the device; we encrypt the text/translatedText fields
// with a per-install key held in chrome.storage.local. Old plaintext rows stay
// readable (decryptStr passes through anything without the "enc:v1:" prefix).
//
// NOTE ON THREAT MODEL: the key lives in chrome.storage.local alongside the
// ciphertext, so this is best-effort obfuscation scoped to the extension origin
// (it protects against casual IndexedDB inspection and other extensions/pages,
// not against a local attacker who can already read this extension's storage).
// Full at-rest protection would require a user-supplied passphrase that is never
// persisted. Because of that, we never silently store plaintext on an encryption
// failure (see encryptStr) — that would defeat the obfuscation without notice.
const _enc = new TextEncoder()
const _dec = new TextDecoder()
let _cryptoKey = null

function _b64(bytes) {
  let s = ''
  bytes.forEach((b) => { s += String.fromCharCode(b) })
  return btoa(s)
}
function _unb64(str) {
  return Uint8Array.from(atob(str), (c) => c.charCodeAt(0))
}

async function getCryptoKey() {
  if (_cryptoKey) return _cryptoKey
  let rawB64
  try {
    const got = await chrome.storage.local.get(['encKey'])
    rawB64 = got.encKey
  } catch { rawB64 = null }
  if (!rawB64) {
    const raw = crypto.getRandomValues(new Uint8Array(32))
    rawB64 = _b64(raw)
    try { await chrome.storage.local.set({ encKey: rawB64 }) } catch {}
  }
  _cryptoKey = await crypto.subtle.importKey('raw', _unb64(rawB64), 'AES-GCM', false, ['encrypt', 'decrypt'])
  return _cryptoKey
}

async function encryptStr(plain) {
  if (plain == null) return plain
  // No plaintext fallback: if encryption fails we throw so the caller skips the
  // write rather than silently persisting unencrypted data.
  const key = await getCryptoKey()
  const iv = crypto.getRandomValues(new Uint8Array(12))
  const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, _enc.encode(String(plain)))
  return `enc:v1:${_b64(iv)}:${_b64(new Uint8Array(ct))}`
}

async function decryptStr(val) {
  if (typeof val !== 'string' || !val.startsWith('enc:v1:')) return val
  try {
    const parts = val.split(':')
    const iv = _unb64(parts[2])
    const ct = _unb64(parts[3])
    const key = await getCryptoKey()
    const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct)
    return _dec.decode(pt)
  } catch {
    return ''
  }
}

function openDB() {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(DB_NAME, DB_VERSION)
    req.onupgradeneeded = (e) => {
      const db = e.target.result
      if (!db.objectStoreNames.contains(STORE_NAME)) {
        const store = db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true })
        store.createIndex('sessionId', 'sessionId', { unique: false })
        store.createIndex('timestamp', 'timestamp', { unique: false })
      }
      if (!db.objectStoreNames.contains(SESSION_STORE)) {
        db.createObjectStore(SESSION_STORE, { keyPath: 'id' })
      }
    }
    req.onsuccess = () => resolve(req.result)
    req.onerror = () => reject(req.error)
  })
}

async function createSession(sessionId, meta) {
  const db = await openDB()
  return new Promise((resolve, reject) => {
    const tx = db.transaction(SESSION_STORE, 'readwrite')
    tx.objectStore(SESSION_STORE).put({
      id: sessionId,
      title: meta?.title || new Date().toLocaleString(),
      sourceLang: meta?.sourceLang || null,
      targetLang: meta?.targetLang || null,
      createdAt: Date.now(),
    })
    tx.oncomplete = () => resolve()
    tx.onerror = () => reject(tx.error)
  })
}

async function updateSession(sessionId, patch) {
  const db = await openDB()
  return new Promise((resolve, reject) => {
    const tx = db.transaction(SESSION_STORE, 'readwrite')
    const store = tx.objectStore(SESSION_STORE)
    const getReq = store.get(sessionId)
    getReq.onsuccess = () => {
      const current = getReq.result || { id: sessionId, createdAt: Date.now() }
      store.put({ ...current, ...patch, id: sessionId })
    }
    getReq.onerror = () => reject(getReq.error)
    tx.oncomplete = () => resolve()
    tx.onerror = () => reject(tx.error)
  })
}

async function saveSegment(sessionId, text, translatedText, timestampMs, relMs) {
  // Encrypt before opening the transaction (IndexedDB tx auto-commits if it
  // sees no pending request while we await crypto). If encryption fails we skip
  // the write entirely rather than storing plaintext.
  let encText, encTrans
  try {
    encText = await encryptStr(text)
    encTrans = translatedText ? await encryptStr(translatedText) : null
  } catch (e) {
    console.warn('[StellarCaptions] segment encryption failed; skipping save', e)
    return
  }
  const db = await openDB()
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_NAME, 'readwrite')
    tx.objectStore(STORE_NAME).add({
      sessionId,
      text: encText,
      translatedText: encTrans,
      timestamp: timestampMs || Date.now(),
      // Recording-relative offset (pause-excluded ms from start). May be
      // undefined for legacy rows; the UI falls back to first-segment time.
      relMs: typeof relMs === 'number' ? relMs : null,
      createdAt: new Date().toISOString(),
    })
    tx.oncomplete = () => resolve()
    tx.onerror = () => reject(tx.error)
  })
}

async function getTranscript(sessionId) {
  const db = await openDB()
  const rows = await new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_NAME, 'readonly')
    const idx = tx.objectStore(STORE_NAME).index('sessionId')
    const req = idx.getAll(sessionId)
    req.onsuccess = () => resolve(req.result.sort((a, b) => a.timestamp - b.timestamp))
    req.onerror = () => reject(req.error)
  })
  for (const r of rows) {
    r.text = await decryptStr(r.text)
    r.translatedText = await decryptStr(r.translatedText)
  }
  return rows
}

async function getAllSessions() {
  const db = await openDB()
  const [segments, sessionMeta] = await Promise.all([
    new Promise((resolve, reject) => {
      const tx = db.transaction(STORE_NAME, 'readonly')
      const req = tx.objectStore(STORE_NAME).getAll()
      req.onsuccess = () => resolve(req.result)
      req.onerror = () => reject(req.error)
    }),
    new Promise((resolve, reject) => {
      const tx = db.transaction(SESSION_STORE, 'readonly')
      const req = tx.objectStore(SESSION_STORE).getAll()
      req.onsuccess = () => resolve(req.result)
      req.onerror = () => reject(req.error)
    }),
  ])

  const metaById = new Map(sessionMeta.map(m => [m.id, m]))
  const counts = new Map()
  const firstTime = new Map()
  const lastTime = new Map()
  segments.forEach(s => {
    counts.set(s.sessionId, (counts.get(s.sessionId) || 0) + 1)
    if (!firstTime.has(s.sessionId)) firstTime.set(s.sessionId, s.timestamp)
    lastTime.set(s.sessionId, s.timestamp)
  })

  // Union of session ids from both stores
  const ids = new Set([...counts.keys(), ...metaById.keys()])
  const result = []
  ids.forEach(id => {
    const meta = metaById.get(id)
    const segCount = counts.get(id) || 0
    if (segCount === 0 && !meta) return
    // Prefer the recorded pause-excluded duration; for legacy sessions fall back
    // to the span between the first and last segment timestamps.
    let durationMs = typeof meta?.durationMs === 'number' ? meta.durationMs : null
    if (durationMs == null && firstTime.has(id) && lastTime.has(id)) {
      durationMs = Math.max(0, lastTime.get(id) - firstTime.get(id))
    }
    result.push({
      id,
      title: meta?.title || new Date(firstTime.get(id) || meta?.createdAt || Date.now()).toLocaleString(),
      sourceLang: meta?.sourceLang || null,
      targetLang: meta?.targetLang || null,
      startTime: firstTime.get(id) || meta?.createdAt || 0,
      segmentCount: segCount,
      durationMs,
    })
  })
  return result.sort((a, b) => b.startTime - a.startTime)
}

async function deleteSession(sessionId) {
  const db = await openDB()
  return new Promise((resolve, reject) => {
    const tx = db.transaction([STORE_NAME, SESSION_STORE], 'readwrite')
    const idx = tx.objectStore(STORE_NAME).index('sessionId')
    const req = idx.openCursor(sessionId)
    req.onsuccess = (e) => {
      const cursor = e.target.result
      if (cursor) { cursor.delete(); cursor.continue() }
    }
    tx.objectStore(SESSION_STORE).delete(sessionId)
    tx.oncomplete = () => resolve()
    tx.onerror = () => reject(tx.error)
  })
}

// Expose on the global scope so this works in both the popup (window) and the
// background service worker (self/globalThis, where there is no `window`).
;(typeof self !== 'undefined' ? self : globalThis).DB = {
  createSession,
  updateSession,
  saveSegment,
  getTranscript,
  getAllSessions,
  deleteSession,
}
