// Tapecast Lokal-Storage (IndexedDB)
// =====================================
// Schema-Mirror der ehemaligen SQLite-Tabellen, alles im Browser.
//
// Stores:
//   - meta:      ID-Generator, Settings (single row id=1)
//   - recordings: Metadaten {id, title, durationSec, createdAt, markers, waveform,
//                            starred, mimeType, fileExt, fileSize, beatSlug}
//   - audio:     Blob pro Recording (key = recording.id)
//
// Wir benutzen kein idb-Wrapper, um die Bundle-Size klein zu halten.

import type { Recording, Settings } from "@shared/schema";

const DB_NAME = "tapecast";
const DB_VERSION = 1;

let dbPromise: Promise<IDBDatabase> | null = null;

function openDb(): Promise<IDBDatabase> {
  if (dbPromise) return dbPromise;
  dbPromise = new Promise((resolve, reject) => {
    const req = indexedDB.open(DB_NAME, DB_VERSION);
    req.onupgradeneeded = () => {
      const db = req.result;
      if (!db.objectStoreNames.contains("meta")) {
        db.createObjectStore("meta");
      }
      if (!db.objectStoreNames.contains("recordings")) {
        const s = db.createObjectStore("recordings", { keyPath: "id" });
        s.createIndex("createdAt", "createdAt");
      }
      if (!db.objectStoreNames.contains("audio")) {
        db.createObjectStore("audio"); // key = recording id (number)
      }
    };
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
  return dbPromise;
}

function tx<T>(stores: string | string[], mode: IDBTransactionMode, fn: (t: IDBTransaction) => Promise<T> | T): Promise<T> {
  return openDb().then(
    (db) =>
      new Promise<T>((resolve, reject) => {
        const t = db.transaction(stores, mode);
        let out: T | Promise<T>;
        try {
          out = fn(t);
        } catch (err) {
          reject(err);
          return;
        }
        t.oncomplete = () => Promise.resolve(out).then(resolve, reject);
        t.onerror = () => reject(t.error);
        t.onabort = () => reject(t.error);
      }),
  );
}

function reqAsPromise<T = unknown>(r: IDBRequest<T>): Promise<T> {
  return new Promise((resolve, reject) => {
    r.onsuccess = () => resolve(r.result);
    r.onerror = () => reject(r.error);
  });
}

// ---------- ID-Generator ----------
async function nextId(t: IDBTransaction): Promise<number> {
  const meta = t.objectStore("meta");
  const cur = ((await reqAsPromise<number | undefined>(meta.get("nextRecordingId"))) as number | undefined) ?? 1;
  meta.put(cur + 1, "nextRecordingId");
  return cur;
}

// ---------- Settings ----------
const DEFAULT_SETTINGS: Settings = {
  id: 1,
  format: "MP3",
  quality: "Hoch",
  storage: "Gerät",
  theme: "dark",
  onboardingDone: 0,
};

export async function getSettings(): Promise<Settings> {
  return tx("meta", "readonly", async (t) => {
    const s = await reqAsPromise<Settings | undefined>(t.objectStore("meta").get("settings") as IDBRequest<Settings | undefined>);
    return s ? { ...DEFAULT_SETTINGS, ...s } : DEFAULT_SETTINGS;
  });
}

export async function patchSettings(patch: Partial<Settings>): Promise<Settings> {
  return tx("meta", "readwrite", async (t) => {
    const store = t.objectStore("meta");
    const cur = ((await reqAsPromise<Settings | undefined>(store.get("settings") as IDBRequest<Settings | undefined>)) ?? DEFAULT_SETTINGS) as Settings;
    const next: Settings = { ...cur, ...patch, id: 1 };
    store.put(next, "settings");
    return next;
  });
}

// ---------- Recordings (Metadata) ----------
export async function listRecordings(): Promise<Recording[]> {
  return tx("recordings", "readonly", async (t) => {
    const arr = await reqAsPromise<Recording[]>(t.objectStore("recordings").getAll() as IDBRequest<Recording[]>);
    return arr.sort((a, b) => b.createdAt - a.createdAt);
  });
}

export async function getRecording(id: number): Promise<Recording | null> {
  return tx("recordings", "readonly", async (t) => {
    const r = await reqAsPromise<Recording | undefined>(t.objectStore("recordings").get(id) as IDBRequest<Recording | undefined>);
    return r ?? null;
  });
}

export interface CreateRecordingInput {
  title: string;
  durationSec: number;
  markers?: string;
  waveform?: string;
  starred?: number;
  mimeType?: string;
  fileExt?: string;
  fileSize?: number;
  beatSlug?: string | null;
}

export async function createRecording(input: CreateRecordingInput): Promise<Recording> {
  return tx(["recordings", "meta"], "readwrite", async (t) => {
    const id = await nextId(t);
    const rec: Recording = {
      id,
      title: input.title,
      durationSec: input.durationSec,
      createdAt: Date.now(),
      markers: input.markers ?? "[]",
      waveform: input.waveform ?? "[]",
      starred: input.starred ?? 0,
      mimeType: input.mimeType ?? "audio/webm",
      fileExt: input.fileExt ?? "webm",
      fileSize: input.fileSize ?? 0,
      beatSlug: input.beatSlug ?? null,
    };
    t.objectStore("recordings").put(rec);
    return rec;
  });
}

export async function updateRecording(id: number, patch: Partial<Recording>): Promise<Recording | null> {
  return tx("recordings", "readwrite", async (t) => {
    const store = t.objectStore("recordings");
    const cur = (await reqAsPromise<Recording | undefined>(store.get(id) as IDBRequest<Recording | undefined>)) as Recording | undefined;
    if (!cur) return null;
    const next: Recording = { ...cur, ...patch, id: cur.id };
    store.put(next);
    return next;
  });
}

export async function deleteRecording(id: number): Promise<boolean> {
  return tx(["recordings", "audio"], "readwrite", async (t) => {
    t.objectStore("recordings").delete(id);
    t.objectStore("audio").delete(id);
    return true;
  });
}

// ---------- Audio Blobs ----------
export async function putAudio(id: number, blob: Blob): Promise<void> {
  await tx("audio", "readwrite", (t) => {
    t.objectStore("audio").put(blob, id);
    return undefined;
  });
}

export async function getAudio(id: number): Promise<Blob | null> {
  return tx("audio", "readonly", async (t) => {
    const b = (await reqAsPromise<Blob | undefined>(t.objectStore("audio").get(id) as IDBRequest<Blob | undefined>)) as Blob | undefined;
    return b ?? null;
  });
}

// ---------- Beats (statisch, kein DB-Eintrag nötig) ----------
export interface Beat {
  slug: string;
  title: string;
  bpm: number;
  key: string;
  url: string;
  durationSec: number;
}

// Pfad relativ zur App, base="./" → wird vom Browser korrekt aufgelöst.
export const BEATS: Beat[] = [
  { slug: "midnight-trap", title: "MIDNIGHT TRAP", bpm: 140, key: "Fm", url: "beats/midnight-trap.mp3", durationSec: 13.71 },
  { slug: "boom-bap",      title: "BOOM BAP",      bpm: 92,  key: "Am", url: "beats/boom-bap.mp3",      durationSec: 20.87 },
  { slug: "lofi-bounce",   title: "LO-FI BOUNCE",  bpm: 85,  key: "C",  url: "beats/lofi-bounce.mp3",   durationSec: 22.59 },
];

// ---------- Object-URL Cache (für <audio src> + Download) ----------
const audioUrlCache = new Map<number, string>();

export async function audioObjectUrl(id: number): Promise<string | null> {
  const cached = audioUrlCache.get(id);
  if (cached) return cached;
  const blob = await getAudio(id);
  if (!blob) return null;
  const url = URL.createObjectURL(blob);
  audioUrlCache.set(id, url);
  return url;
}

export function revokeAudioObjectUrl(id: number): void {
  const url = audioUrlCache.get(id);
  if (url) {
    URL.revokeObjectURL(url);
    audioUrlCache.delete(id);
  }
}

export function clearAllAudioUrls(): void {
  for (const url of audioUrlCache.values()) URL.revokeObjectURL(url);
  audioUrlCache.clear();
}

// Storage-Estimate für Settings-Display (optional).
export async function storageEstimate(): Promise<{ usedMB: number; quotaMB: number } | null> {
  if (!navigator.storage?.estimate) return null;
  try {
    const e = await navigator.storage.estimate();
    return {
      usedMB: Math.round(((e.usage ?? 0) / (1024 * 1024)) * 10) / 10,
      quotaMB: Math.round(((e.quota ?? 0) / (1024 * 1024)) * 10) / 10,
    };
  } catch {
    return null;
  }
}
