/**
 * Lane J — the local override store.
 *
 * Database/store names come from lanes/LANE-I §Deliverables 1 ('blobbo-workshop'
 * with `manifest` + `blobs`): the editor WRITES here and the shipped game READS
 * here, which is what makes a custom asset playable on the live site with no
 * deploy. The manifest is a single record under MANIFEST_KEY; GLB bytes live in
 * `blobs` under the key embedded in each `idb:<key>` url.
 *
 * Raw IDB rather than a wrapper because no new deps are allowed, and the surface
 * needed here is four calls wide.
 *
 * Two rules this module exists to enforce:
 *   - The database NAME is a variable. The demo points itself at a throwaway
 *     database so running it can never touch a real saved swap.
 *   - A failed open is never memoised. A private window, a blocked upgrade or a
 *     momentarily unavailable disk would otherwise dead-end the whole session.
 */

export const DEFAULT_DB_NAME = 'blobbo-workshop'
/** The database the live game reads. Kept exported: existing callers import it. */
export const DB_NAME = DEFAULT_DB_NAME
export const DB_VERSION = 1
export const MANIFEST_STORE = 'manifest'
export const BLOB_STORE = 'blobs'
/** The single record in the `manifest` store that holds the working manifest. */
export const MANIFEST_KEY = 'current'

/** How long a blocked upgrade may hang before we call storage unusable. */
const OPEN_TIMEOUT_MS = 4000

let dbName = DEFAULT_DB_NAME
let dbPromise: Promise<IDBDatabase> | null = null
let availability: Promise<boolean> | null = null

/** Which database this process talks to. Switching drops every cached handle. */
export function setDbName(name: string): void {
  if (name === dbName) return
  dbName = name
  dbPromise = null
  availability = null
}

export const getDbName = (): string => dbName

/** Throw away the cached connection and the cached availability answer. */
export function resetIdb(): void {
  dbPromise = null
  availability = null
}

function openDb(): Promise<IDBDatabase> {
  if (dbPromise) return dbPromise
  const attempt = new Promise<IDBDatabase>((resolve, reject) => {
    if (typeof indexedDB === 'undefined' || indexedDB === null) {
      reject(new Error('This browser has no local storage for models.'))
      return
    }
    let settled = false
    const done = (fn: () => void): void => {
      if (settled) return
      settled = true
      fn()
    }
    // A blocked upgrade fires neither success nor error — without this the
    // caller waits forever and the UI never leaves its "reading…" state.
    const timer = setTimeout(
      () => done(() => reject(new Error('Local storage did not respond.'))),
      OPEN_TIMEOUT_MS,
    )
    const finish = (fn: () => void): void => done(() => { clearTimeout(timer); fn() })
    let req: IDBOpenDBRequest
    try {
      req = indexedDB.open(dbName, DB_VERSION)
    } catch (err) {
      finish(() => reject(err instanceof Error ? err : new Error(String(err))))
      return
    }
    req.onupgradeneeded = () => {
      const db = req.result
      if (!db.objectStoreNames.contains(MANIFEST_STORE)) db.createObjectStore(MANIFEST_STORE)
      if (!db.objectStoreNames.contains(BLOB_STORE)) db.createObjectStore(BLOB_STORE)
    }
    req.onsuccess = () => finish(() => resolve(req.result))
    req.onerror = () => finish(() => reject(req.error ?? new Error('IndexedDB open failed')))
    req.onblocked = () => finish(() => reject(new Error('Another tab is using this storage.')))
  })
  // Memoise the SUCCESS only: a cached rejection makes one bad moment permanent.
  dbPromise = attempt
  attempt.catch(() => { if (dbPromise === attempt) dbPromise = null })
  return attempt
}

function run<T>(
  store: string,
  mode: IDBTransactionMode,
  fn: (s: IDBObjectStore) => IDBRequest<T>,
): Promise<T> {
  return openDb().then(
    (db) =>
      new Promise<T>((resolve, reject) => {
        let req: IDBRequest<T>
        try {
          const tx = db.transaction(store, mode)
          req = fn(tx.objectStore(store))
        } catch (err) {
          reject(err instanceof Error ? err : new Error(String(err)))
          return
        }
        req.onsuccess = () => resolve(req.result)
        req.onerror = () => reject(req.error ?? new Error('IndexedDB request failed'))
      }),
  )
}

export const idbGet = <T>(store: string, key: string): Promise<T | undefined> =>
  run<T | undefined>(store, 'readonly', (s) => s.get(key) as IDBRequest<T | undefined>)

export const idbPut = (store: string, key: string, value: unknown): Promise<unknown> =>
  run(store, 'readwrite', (s) => s.put(value, key) as IDBRequest<unknown>)

export const idbDelete = (store: string, key: string): Promise<unknown> =>
  run(store, 'readwrite', (s) => s.delete(key) as IDBRequest<unknown>)

export const idbKeys = (store: string): Promise<IDBValidKey[]> =>
  run<IDBValidKey[]>(store, 'readonly', (s) => s.getAllKeys())

export const idbClear = (store: string): Promise<unknown> =>
  run(store, 'readwrite', (s) => s.clear() as IDBRequest<unknown>)

/**
 * True when this browser can actually hold overrides. `typeof indexedDB` is not
 * an answer: Safari/Firefox private windows expose the object and then fail the
 * open, which is exactly the case the editor has to survive. So open it once for
 * real and cache the BOOLEAN.
 */
export function idbAvailable(): Promise<boolean> {
  if (!availability) availability = openDb().then(() => true, () => false)
  return availability
}

/** Delete a whole database — the demo's cleanup, never used on the real one. */
export function deleteDb(name: string): Promise<void> {
  return new Promise((resolve) => {
    if (typeof indexedDB === 'undefined' || indexedDB === null) return resolve()
    if (name === dbName) resetIdb()
    let req: IDBOpenDBRequest
    try {
      req = indexedDB.deleteDatabase(name)
    } catch {
      return resolve()
    }
    req.onsuccess = () => resolve()
    req.onerror = () => resolve()
    req.onblocked = () => resolve()
  })
}
