// Session + preferences persistence. The session token lives in memory for the app
// session and is mirrored to IndexedDB when available (so a reload does not force a
// re-login), with a localStorage fallback. Losing the store just means re-login —
// acceptable, and for a guest it simply mints a fresh guest.

import type { Session, Profile } from './model';
import type { ThemePref } from './theme';
import type { Locale } from './i18n/catalog';
import type { BoardLabelMode } from './boardlabels';

const DB_NAME = 'scrabble';
const STORE = 'kv';
const LS_PREFIX = 'scrabble.';

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

function openDb(): Promise<IDBDatabase> | null {
  if (dbPromise !== undefined) return dbPromise;
  if (typeof indexedDB === 'undefined') {
    dbPromise = null;
    return null;
  }
  dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
    const req = indexedDB.open(DB_NAME, 1);
    req.onupgradeneeded = () => req.result.createObjectStore(STORE);
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  }).catch(() => {
    dbPromise = null;
    throw new Error('indexedDB unavailable');
  });
  return dbPromise;
}

function lsGet<T>(key: string): T | null {
  try {
    const v = localStorage.getItem(LS_PREFIX + key);
    return v ? (JSON.parse(v) as T) : null;
  } catch {
    return null;
  }
}

function lsSet(key: string, value: unknown): void {
  try {
    localStorage.setItem(LS_PREFIX + key, JSON.stringify(value));
  } catch {
    /* storage unavailable — stay in-memory only */
  }
}

function lsDel(key: string): void {
  try {
    localStorage.removeItem(LS_PREFIX + key);
  } catch {
    /* ignore */
  }
}

async function kvGet<T>(key: string): Promise<T | null> {
  const db = openDb();
  if (!db) return lsGet<T>(key);
  try {
    const d = await db;
    return await new Promise<T | null>((resolve, reject) => {
      const r = d.transaction(STORE, 'readonly').objectStore(STORE).get(key);
      r.onsuccess = () => resolve((r.result ?? null) as T | null);
      r.onerror = () => reject(r.error);
    });
  } catch {
    return lsGet<T>(key);
  }
}

async function kvSet(key: string, value: unknown): Promise<void> {
  const db = openDb();
  if (!db) return lsSet(key, value);
  try {
    const d = await db;
    await new Promise<void>((resolve, reject) => {
      const tx = d.transaction(STORE, 'readwrite');
      tx.objectStore(STORE).put(value, key);
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
    });
  } catch {
    lsSet(key, value);
  }
}

async function kvDel(key: string): Promise<void> {
  const db = openDb();
  if (!db) return lsDel(key);
  try {
    const d = await db;
    await new Promise<void>((resolve, reject) => {
      const tx = d.transaction(STORE, 'readwrite');
      tx.objectStore(STORE).delete(key);
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
    });
  } catch {
    lsDel(key);
  }
}

const SESSION_KEY = 'session';
const PREFS_KEY = 'prefs';

export function loadSession(): Promise<Session | null> {
  return kvGet<Session>(SESSION_KEY);
}

export function saveSession(s: Session): Promise<void> {
  return kvSet(SESSION_KEY, s);
}

export function clearSession(): Promise<void> {
  return kvDel(SESSION_KEY);
}

const PROFILE_KEY = 'profile';

/** loadProfile reads the last persisted profile (or null). It backs the offline cold-boot: with no
 *  network, bootstrap uses it instead of fetching a fresh one. */
export function loadProfile(): Promise<Profile | null> {
  return kvGet<Profile>(PROFILE_KEY);
}

/** saveProfile persists the caller's own profile so a later offline launch can start from it (its
 *  variant preferences, dictionary versions and display name), best-effort. */
export function saveProfile(p: Profile): Promise<void> {
  return kvSet(PROFILE_KEY, p);
}

/** clearProfile drops the persisted profile (on logout, with the session). */
export function clearProfile(): Promise<void> {
  return kvDel(PROFILE_KEY);
}

export interface Prefs {
  theme: ThemePref;
  locale: Locale;
  reduceMotion: boolean;
  boardLabels: BoardLabelMode;
  zoomBoard: boolean;
}

export async function loadPrefs(): Promise<Partial<Prefs>> {
  return (await kvGet<Prefs>(PREFS_KEY)) ?? {};
}

export function savePrefs(p: Prefs): Promise<void> {
  return kvSet(PREFS_KEY, p);
}

const ONBOARDING_KEY = 'onboarding';

/**
 * Whether each first-run coachmark series has been completed. A series is marked done only after
 * its last hint, so a player interrupted mid-series sees it again from the start next launch.
 */
export interface OnboardingState {
  lobbyDone: boolean;
  gameDone: boolean;
}

/** Loads the persisted onboarding completion flags, defaulting to not-yet-seen. */
export async function loadOnboarding(): Promise<OnboardingState> {
  const s = await kvGet<Partial<OnboardingState>>(ONBOARDING_KEY);
  return { lobbyDone: s?.lobbyDone ?? false, gameDone: s?.gameDone ?? false };
}

/** Persists the onboarding completion flags. */
export function saveOnboarding(s: OnboardingState): Promise<void> {
  return kvSet(ONBOARDING_KEY, s);
}

/** Clears the onboarding flags so the coachmarks replay on the next launch (DebugPanel reset). */
export function clearOnboarding(): Promise<void> {
  return kvDel(ONBOARDING_KEY);
}
