/**
 * Lane J — working-manifest persistence on top of idb.ts.
 *
 * Two things live here: the manifest the game reads on boot, and the raw GLB
 * bytes it references via `idb:<key>` urls. Everything is tolerant — a corrupt
 * record reads back as an empty manifest rather than blocking the editor.
 */
import {
  BLOB_STORE, MANIFEST_KEY, MANIFEST_STORE,
  idbClear, idbDelete, idbGet, idbKeys, idbPut,
} from './idb'
import { cleanManifest, idbKey, isIdbUrl, parseManifest } from './manifest-io'
import type { Manifest } from './manifest-io'

/**
 * What a `blobs` record reads back as.
 *
 * On disk the record is the RAW ArrayBuffer, because that is what the shipped
 * game's reader expects (src/assets/idb.ts `getBlob` feeds its result straight
 * into `new Blob([buf])`). An earlier version wrote a `{name, bytes, savedAt}`
 * wrapper here, which the game could not read at all — so reads stay tolerant of
 * that shape and writes never produce it again. The file name is recoverable
 * from the key, which is `<slot>-<filename>`.
 */
export interface StoredGlb {
  name: string
  bytes: ArrayBuffer
  savedAt: number
}

/** Legacy wrapper shape, still on disk for anyone who saved before the fix. */
interface LegacyStoredGlb {
  name?: unknown
  bytes?: unknown
  savedAt?: unknown
}

export async function loadOverrideManifest(): Promise<Manifest> {
  const raw = await idbGet<unknown>(MANIFEST_STORE, MANIFEST_KEY)
  if (typeof raw === 'string') return parseManifest(raw)
  if (raw && typeof raw === 'object') return parseManifest(JSON.stringify(raw))
  return {}
}

/** Stored as an object (not a string) so Lane I's loader can read it either way. */
export const saveOverrideManifest = (manifest: Manifest): Promise<unknown> =>
  idbPut(MANIFEST_STORE, MANIFEST_KEY, cleanManifest(manifest))

/** A stable, human-readable blob key: `<slot>-<filename>`. */
export const blobKeyFor = (slot: string, fileName: string): string =>
  `${slot}-${fileName}`.replace(/[^a-zA-Z0-9._-]/g, '_')

export async function putGlb(key: string, _name: string, bytes: ArrayBuffer): Promise<string> {
  await idbPut(BLOB_STORE, key, bytes)
  return `idb:${key}`
}

export async function getGlb(key: string): Promise<StoredGlb | undefined> {
  const raw = await idbGet<unknown>(BLOB_STORE, key)
  if (raw instanceof ArrayBuffer) {
    return { name: nameFromKey(key), bytes: raw, savedAt: 0 }
  }
  const legacy = raw as LegacyStoredGlb | undefined
  if (legacy && legacy.bytes instanceof ArrayBuffer) {
    return {
      name: typeof legacy.name === 'string' ? legacy.name : nameFromKey(key),
      bytes: legacy.bytes,
      savedAt: typeof legacy.savedAt === 'number' ? legacy.savedAt : 0,
    }
  }
  return undefined
}

/** `<slot>-<filename>` back to `<filename>` — best effort, display only. */
const nameFromKey = (key: string): string => {
  const dash = key.lastIndexOf('-')
  return dash >= 0 ? key.slice(dash + 1) : key
}

/** Every override gone: manifest record and all stored GLB bytes. */
export async function clearOverrides(): Promise<void> {
  await idbDelete(MANIFEST_STORE, MANIFEST_KEY)
  await idbClear(BLOB_STORE)
}

/**
 * Blob keys with nothing pointing at them any more (drop-then-reset leftovers).
 *
 * CONSTRAINT: whatever manifest is handed in decides what survives, so a caller
 * must never hand in a manifest it has already pruned entries from. Workshop
 * keeps entries whose file failed to load precisely so their bytes are not
 * counted as orphans here — dropping them would destroy an unrecoverable file.
 */
export async function orphanBlobKeys(manifest: Manifest): Promise<string[]> {
  const live = new Set(
    Object.values(manifest).filter((e) => isIdbUrl(e.url)).map((e) => idbKey(e.url)),
  )
  const keys = await idbKeys(BLOB_STORE)
  return keys.map(String).filter((k) => !live.has(k))
}

export async function pruneOrphans(manifest: Manifest): Promise<number> {
  const orphans = await orphanBlobKeys(manifest)
  for (const k of orphans) await idbDelete(BLOB_STORE, k)
  return orphans.length
}
