import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from 'matrix-js-sdk';

import { cryptoCallbacks } from './secretStorageKeys';
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
import { getFallbackSession, removeFallbackSession, Session } from '../app/state/sessions';
import { LotusOidcTokenRefresher } from './oidcTokenRefresher';
import { revokeOidcTokens } from './oidcLogout';
import { pushSessionToSW } from '../sw-session';
import { deleteSearchCacheDatabase } from '../app/utils/searchCache';
import { clearPlaintextCaches } from '../app/state/plaintextCaches';

// Thrown when the local IndexedDB has a higher schema version than this SDK expects.
// This happens after a downgrade (e.g. matrix-js-sdk was briefly upgraded and then reverted).
export const IDB_VERSION_CONFLICT = 'IDB_VERSION_CONFLICT';

/**
 * KE-1 mitigation. Ask the browser to make this origin's storage persistent so the
 * IndexedDB **crypto store** isn't evicted from under a surviving `localStorage`
 * session. When that happens the device "resurrects" with a blank key store and the
 * client re-uploads a one-time key at an id Synapse already holds → a permanent
 * `400 M_UNKNOWN: … already exists` upload-conflict storm (and, downstream,
 * undecryptable to-device/media keys). `persist()` grants based on engagement / PWA
 * install and shows no prompt; denial/absence is non-fatal.
 */
export const requestPersistentStorage = async (): Promise<boolean> => {
  try {
    if (!navigator.storage?.persist) return false;
    if (await navigator.storage.persisted()) return true;
    const granted = await navigator.storage.persist();
    if (!granted) {
      console.warn('Persistent storage not granted — the crypto store remains evictable (KE-1).');
    }
    return granted;
  } catch {
    return false;
  }
};

export const initClient = async (session: Session): Promise<MatrixClient> => {
  // Protect the crypto store from eviction before we create and write to it.
  await requestPersistentStorage();

  const indexedDBStore = new IndexedDBStore({
    indexedDB: globalThis.indexedDB,
    localStorage: globalThis.localStorage,
    dbName: 'web-sync-store',
  });

  const legacyCryptoStore = new IndexedDBCryptoStore(globalThis.indexedDB, 'crypto-store');

  // OIDC/next-gen-auth sessions carry a refresh token; wire automatic refresh
  // (the client calls this reactively on a 401) and persist rotated tokens.
  const oidcRefresher =
    session.refreshToken && session.oidc
      ? new LotusOidcTokenRefresher(session.oidc, session.deviceId, session.userId, session.baseUrl)
      : undefined;

  const mx = createClient({
    baseUrl: session.baseUrl,
    accessToken: session.accessToken,
    refreshToken: session.refreshToken,
    userId: session.userId,
    store: indexedDBStore,
    cryptoStore: legacyCryptoStore,
    deviceId: session.deviceId,
    timelineSupport: true,
    cryptoCallbacks: cryptoCallbacks as any,
    // SAS (emoji) + QR-code verification (show/scan/reciprocate).
    verificationMethods: ['m.sas.v1', 'm.qr_code.show.v1', 'm.qr_code.scan.v1', 'm.reciprocate.v1'],
    tokenRefreshFunction: oidcRefresher
      ? (refreshToken) => oidcRefresher.doRefreshAccessToken(refreshToken)
      : undefined,
  });

  try {
    await indexedDBStore.startup();
  } catch (e) {
    // IDB VersionError = local DB was written by a newer SDK version (schema downgrade).
    if (e instanceof DOMException && e.name === 'VersionError') {
      throw new Error(IDB_VERSION_CONFLICT);
    }
    throw e;
  }
  await mx.initRustCrypto();

  mx.setMaxListeners(150);
  mx.matrixRTC.setMaxListeners(150);

  return mx;
};

export const startClient = async (mx: MatrixClient) => {
  await mx.startClient({
    lazyLoadMembers: true,
    // P3-8: partition m.thread relations into Thread objects/timelines. Thread
    // replies leave the main timeline (roots stay + get a summary chip); the
    // thread panel renders them. markAsRead sends UNTHREADED receipts
    // (utils/notifications.ts) so room badges keep clearing.
    threadSupport: true,
  });
};

export const clearCacheAndReload = async (mx: MatrixClient) => {
  mx.stopClient();
  clearNavToActivePathStore(mx.getSafeUserId());
  await mx.store.deleteAllData();
  window.location.reload();
};

export const logoutClient = async (mx: MatrixClient) => {
  pushSessionToSW();
  mx.stopClient();
  // For OIDC sessions, revoke the tokens at the issuer too (best-effort).
  const session = getFallbackSession();
  if (session?.oidc) {
    await revokeOidcTokens(session);
  }
  try {
    await mx.logout();
  } catch {
    // ignore if failed to logout
  }
  await mx.clearStores();
  // The opt-in local search index stores decrypted plaintext — always wipe it
  // on logout. (clearLoginData below nukes all IDB databases, covering it too.)
  await deleteSearchCacheDatabase();
  // Other localStorage caches also hold decrypted plaintext / PII (translation,
  // scheduled messages, recent searches/forwards/gifs/stickers, nav paths) —
  // wipe them too.
  clearPlaintextCaches(mx.getUserId() ?? undefined);
  // Remove only the session credential keys, preserving user preferences and
  // unsent drafts (N98). The factory-reset path is clearLoginData() below.
  removeFallbackSession();
  window.location.reload();
};

export const clearLoginData = async () => {
  const dbs = await window.indexedDB.databases();

  dbs.forEach((idbInfo) => {
    const { name } = idbInfo;
    if (name) {
      window.indexedDB.deleteDatabase(name);
    }
  });

  // Unregister service workers so stale caches don't interfere after a reset
  if ('serviceWorker' in navigator) {
    const regs = await navigator.serviceWorker.getRegistrations();
    await Promise.all(regs.map((r) => r.unregister()));
    const cacheNames = await caches.keys();
    await Promise.all(cacheNames.map((c) => caches.delete(c)));
  }

  window.localStorage.clear();
  window.location.reload();
};
