import { openDB, type DBSchema, type IDBPDatabase } from 'idb';

type StationCatalogKey = 'radio-browser' | 'managed';

type StationCatalogRecord = {
  key: StationCatalogKey;
  stations: unknown[];
  source: string | null;
  updatedAt: string;
};

interface StationCatalogDb extends DBSchema {
  catalogs: {
    key: StationCatalogKey;
    value: StationCatalogRecord;
  };
}

const DB_NAME = 'radioplayer-station-catalogs';
const DB_VERSION = 1;
const LOCAL_STORAGE_KEYS = {
  radioBrowser: 'radioplayer.stationCatalog.radioBrowser',
  managed: 'radioplayer.stationCatalog.managed',
};

const BUILD_ID_KEY = 'radioplayer.stationCatalog.buildId';

let dbPromise: Promise<IDBPDatabase<StationCatalogDb> | null> | null = null;

function getDb() {
  if (!dbPromise) {
    dbPromise = openDB<StationCatalogDb>(DB_NAME, DB_VERSION, {
      upgrade(db) {
        if (!db.objectStoreNames.contains('catalogs')) {
          db.createObjectStore('catalogs', { keyPath: 'key' });
        }
      },
    }).catch((error) => {
      console.warn('Station catalog IndexedDB unavailable, using localStorage fallback.', error);
      return null;
    });
  }

  return dbPromise;
}

function getLocalStorage(): Storage | null {
  try {
    return globalThis.localStorage ?? null;
  } catch {
    return null;
  }
}

function getLocalStorageKey(key: StationCatalogKey) {
  return key === 'managed' ? LOCAL_STORAGE_KEYS.managed : LOCAL_STORAGE_KEYS.radioBrowser;
}

function sanitizeStations(value: unknown): unknown[] {
  return Array.isArray(value) ? value : [];
}

function sanitizeSource(value: unknown): string | null {
  return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
}

function sanitizeCatalogRecord(key: StationCatalogKey, value: unknown): StationCatalogRecord | null {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return null;
  }

  const record = value as Partial<StationCatalogRecord>;
  return {
    key,
    stations: sanitizeStations(record.stations),
    source: sanitizeSource(record.source),
    updatedAt: typeof record.updatedAt === 'string' && record.updatedAt.trim().length > 0
      ? record.updatedAt
      : new Date(0).toISOString(),
  };
}

function readCatalogFromLocalStorage(key: StationCatalogKey): StationCatalogRecord | null {
  const storage = getLocalStorage();
  if (!storage) {
    return null;
  }

  const raw = storage.getItem(getLocalStorageKey(key));
  if (!raw) {
    return null;
  }

  try {
    return sanitizeCatalogRecord(key, JSON.parse(raw));
  } catch {
    return null;
  }
}

function writeCatalogToLocalStorage(record: StationCatalogRecord) {
  const storage = getLocalStorage();
  if (!storage) {
    return;
  }

  try {
    storage.setItem(getLocalStorageKey(record.key), JSON.stringify(record));
  } catch {
    // Ignore quota/write failures and rely on IndexedDB when available.
  }
}

function getStoredBuildId(): string | null {
  const storage = getLocalStorage();
  if (!storage) return null;
  try {
    return storage.getItem(BUILD_ID_KEY);
  } catch {
    return null;
  }
}

function setStoredBuildId(buildId: string) {
  const storage = getLocalStorage();
  if (!storage) return;
  try {
    storage.setItem(BUILD_ID_KEY, buildId);
  } catch {
    // ignore
  }
}

async function clearManagedCatalogPersistence() {
  const storage = getLocalStorage();
  if (storage) {
    try {
      storage.removeItem(getLocalStorageKey('managed'));
      storage.removeItem(BUILD_ID_KEY);
    } catch {
      // ignore
    }
  }

  const db = await getDb();
  if (db) {
    try {
      await db.delete('catalogs', 'managed');
    } catch {
      // ignore
    }
  }
}

async function readCatalog(key: StationCatalogKey): Promise<StationCatalogRecord | null> {
  // If a previous build persisted a managed catalog, invalidate it when the
  // current build id differs. This forces clients to fetch the updated managed
  // catalog after a deploy without requiring manual cache clearing.
  if (key === 'managed') {
    const stored = getStoredBuildId();
    if (stored && stored !== __APP_BUILD_ID__) {
      await clearManagedCatalogPersistence();
      return null;
    }
  }

  const db = await getDb();
  if (!db) {
    return readCatalogFromLocalStorage(key);
  }

  try {
    const record = await db.get('catalogs', key);
    return sanitizeCatalogRecord(key, record);
  } catch {
    return readCatalogFromLocalStorage(key);
  }
}

async function writeCatalog(key: StationCatalogKey, stations: unknown[], source: string | null = null) {
  const record: StationCatalogRecord = {
    key,
    stations: sanitizeStations(stations),
    source: sanitizeSource(source),
    updatedAt: new Date().toISOString(),
  };

  writeCatalogToLocalStorage(record);

  // Persist build id for managed catalog so we can invalidate on next run.
  if (key === 'managed') {
    setStoredBuildId(__APP_BUILD_ID__);
  }

  const db = await getDb();
  if (!db) {
    return;
  }

  try {
    await db.put('catalogs', record);
  } catch {
    // Keep the localStorage mirror if IndexedDB writes fail.
  }
}

export async function loadPersistedRadioStationCatalog(): Promise<unknown[] | null> {
  const record = await readCatalog('radio-browser');
  return record ? record.stations : null;
}

export async function persistRadioStationCatalog(stations: unknown[]) {
  await writeCatalog('radio-browser', stations);
}

export async function loadPersistedManagedStationCatalog(): Promise<{ stations: unknown[]; source: string | null } | null> {
  const record = await readCatalog('managed');
  return record
    ? {
      stations: record.stations,
      source: record.source,
    }
    : null;
}

export async function persistManagedStationCatalog(stations: unknown[], source: string | null) {
  await writeCatalog('managed', stations, source);
}

export async function clearPersistedManagedStationCatalog(): Promise<void> {
  await clearManagedCatalogPersistence();
}