/**
 * Lane I — local override store (`blobbo-workshop`).
 *
 * Why IndexedDB and not a server: John must be able to try a custom asset on
 * the LIVE site with zero deploys. The editor writes a manifest + the raw .glb
 * bytes here; the game reads them at boot and they win over the shipped pack.
 *
 * Two stores:
 *   `manifest` — single record under key 'current', a Manifest object.
 *   `blobs`    — key -> ArrayBuffer of a .glb, addressed as `idb:<key>` urls.
 *
 * Every function resolves rather than rejects when IndexedDB is unavailable
 * (private mode, old browser, node): no override is a normal state, not an error.
 */
import { validateManifest, type Manifest } from './manifest'

export const DB_NAME = 'blobbo-workshop'
export const DB_VERSION = 1
export const STORE_MANIFEST = 'manifest'
export const STORE_BLOBS = 'blobs'
const MANIFEST_KEY = 'current'

export const IDB_URL_PREFIX = 'idb:'

export function isIdbUrl(url: string): boolean {
  return url.startsWith(IDB_URL_PREFIX)
}

export function idbKeyFromUrl(url: string): string {
  return url.slice(IDB_URL_PREFIX.length)
}

function openDb(): Promise<IDBDatabase | null> {
  return new Promise((resolve) => {
    if (typeof indexedDB === 'undefined') return resolve(null)
    let req: IDBOpenDBRequest
    try {
      req = indexedDB.open(DB_NAME, DB_VERSION)
    } catch {
      return resolve(null)
    }
    req.onupgradeneeded = () => {
      const db = req.result
      if (!db.objectStoreNames.contains(STORE_MANIFEST)) db.createObjectStore(STORE_MANIFEST)
      if (!db.objectStoreNames.contains(STORE_BLOBS)) db.createObjectStore(STORE_BLOBS)
    }
    req.onsuccess = () => resolve(req.result)
    req.onerror = () => resolve(null)
    req.onblocked = () => resolve(null)
  })
}

function request<T>(store: IDBObjectStore, run: (s: IDBObjectStore) => IDBRequest): Promise<T | null> {
  return new Promise((resolve) => {
    let req: IDBRequest
    try {
      req = run(store)
    } catch {
      return resolve(null)
    }
    req.onsuccess = () => resolve(req.result as T)
    req.onerror = () => resolve(null)
  })
}

async function withStore<T>(
  name: string,
  mode: IDBTransactionMode,
  run: (s: IDBObjectStore) => IDBRequest,
): Promise<T | null> {
  const db = await openDb()
  if (!db) return null
  try {
    const tx = db.transaction(name, mode)
    const out = await request<T>(tx.objectStore(name), run)
    return out
  } catch {
    return null
  } finally {
    db.close()
  }
}

/** The editor's saved pack, or `{}` when there is none. Never throws. */
export async function loadOverrideManifest(): Promise<Manifest> {
  const raw = await withStore<unknown>(STORE_MANIFEST, 'readonly', (s) => s.get(MANIFEST_KEY))
  if (raw === null || raw === undefined) return {}
  const { manifest, problems } = validateManifest(raw)
  if (problems.length) console.warn('[assets] local override manifest problems:', problems)
  return manifest
}

export async function saveOverrideManifest(manifest: Manifest): Promise<boolean> {
  const out = await withStore<IDBValidKey>(STORE_MANIFEST, 'readwrite', (s) =>
    s.put(manifest, MANIFEST_KEY),
  )
  return out !== null
}

export async function clearOverrides(): Promise<void> {
  await withStore(STORE_MANIFEST, 'readwrite', (s) => s.clear())
  await withStore(STORE_BLOBS, 'readwrite', (s) => s.clear())
}

export async function putBlob(key: string, data: ArrayBuffer): Promise<boolean> {
  const out = await withStore<IDBValidKey>(STORE_BLOBS, 'readwrite', (s) => s.put(data, key))
  return out !== null
}

export async function getBlob(key: string): Promise<ArrayBuffer | null> {
  return withStore<ArrayBuffer>(STORE_BLOBS, 'readonly', (s) => s.get(key))
}

export async function listBlobKeys(): Promise<string[]> {
  const keys = await withStore<IDBValidKey[]>(STORE_BLOBS, 'readonly', (s) => s.getAllKeys())
  return (keys ?? []).map(String)
}

/** True when a local pack is active — the game shows a "custom assets" pill. */
export async function hasOverrides(): Promise<boolean> {
  const m = await loadOverrideManifest()
  return Object.keys(m).length > 0
}
