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

import { cryptoCallbacks } from './secretStorageKeys';
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
import { pushSessionToSW } from '../sw-session';
import { clearPusherIds, loadPusherIds, setPushEnabled, unregisterPusher } from '../app/utils/push';
import { isNativePlatform } from '../app/utils/capacitor';
import { clearSessionBridge } from '../app/utils/sessionBridge';
import { polling } from '../app/plugins/polling';
import { proxy } from '../app/plugins/proxy';

type Session = {
  baseUrl: string;
  accessToken: string;
  userId: string;
  deviceId: string;
};

export const initClient = async (session: Session): Promise<MatrixClient> => {
  const indexedDBStore = new IndexedDBStore({
    indexedDB: global.indexedDB,
    localStorage: global.localStorage,
    dbName: 'web-sync-store',
  });

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

  const mx = createClient({
    baseUrl: session.baseUrl,
    accessToken: session.accessToken,
    userId: session.userId,
    store: indexedDBStore,
    cryptoStore: legacyCryptoStore,
    deviceId: session.deviceId,
    timelineSupport: true,
    // cryptoCallbacks ships from a .js module with a duck-typed shape; matrix-js-sdk's exported type is too narrow.
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    cryptoCallbacks: cryptoCallbacks as any,
    verificationMethods: ['m.sas.v1'],
  });

  await indexedDBStore.startup();
  await mx.initRustCrypto();

  mx.setMaxListeners(50);

  return mx;
};

export const startClient = async (mx: MatrixClient) => {
  // threadSupport partitions m.thread relations into Thread objects so
  // /channels thread drawer can read room.getThread(rootId), receive
  // ThreadEvent.New / Update from the room emitter, and let
  // sendReadReceipt auto-route thread_id. SDK 38.2+ default is false:
  // without this flag drawer reads silently NO-OP. Flag is global —
  // also affects DM/Bots receipt shape (SDK now adds thread_id: 'main'
  // to main-timeline receipts and per-thread unread shape changes
  // arrive in `unread_thread_notifications`). M4 will consume the
  // shape; M2 just enables it so the drawer surface works.
  await mx.startClient({
    lazyLoadMembers: true,
    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) => {
  // 1. Deactivate pusher on the homeserver while we still have a valid token.
  //    Run before pushSessionToSW() so that if a push arrives mid-logout, the
  //    SW still has a working session to resolve event details with.
  const pusherIds = loadPusherIds();
  if (pusherIds) {
    try {
      await unregisterPusher(mx, pusherIds.pushkey, pusherIds.appId);
    } catch {
      // ignore — logout must proceed
    }
  }

  pushSessionToSW();

  // 2. Unsubscribe locally so the browser/FCM stops delivering to this device.
  //    Critical: if step 1 failed (bad network, server 5xx), this is the only
  //    thing that actually stops pushes from arriving at this installation.
  try {
    if (isNativePlatform()) {
      const { PushNotifications } = await import('@capacitor/push-notifications');
      await PushNotifications.unregister();
    } else if ('serviceWorker' in navigator) {
      const reg = await navigator.serviceWorker.ready;
      const sub = await reg.pushManager.getSubscription();
      if (sub) await sub.unsubscribe();
    }
  } catch {
    // ignore
  }

  clearPusherIds();
  setPushEnabled(false);

  // Tear down the WorkManager polling fallback synchronously here — the
  // React lifecycle cleanup that does the same thing runs async and we
  // don't get a chance to await it before window.location.replace below.
  // Without this explicit await, the Worker can fire one more time with
  // the old access_token and surface notifications belonging to the
  // logged-out account.
  await polling.cancel();
  await polling.clearSession();

  // Wipe the Tink-encrypted proxy creds + clear the WebView override + tear
  // down the relay (proxy_support.md §15: creds are wiped on logout, locked).
  // A blocked re-login re-enters the proxy on the auth screen (pre-login
  // entry, F1). No-op on web.
  await proxy.clearConfig();

  // Wipe the native session bridge so a re-login with a different user
  // can't resurrect the old access_token via CallDeclineReceiver.
  await clearSessionBridge();

  // Tell the homeserver we're going offline before stopClient/logout
  // tears down the access_token. Without this PUT, Synapse waits ~5
  // minutes for its own idle timer to flip the user Offline; peers see
  // a stale "был в сети только что" during that gap. Best-effort —
  // logout must complete even if this fails.
  try {
    await mx.setPresence({ presence: 'offline' });
  } catch {
    // ignore — server will idle-timeout the presence eventually.
  }

  mx.stopClient();
  try {
    await mx.logout();
  } catch {
    // ignore if failed to logout
  }
  // Clear the credential FIRST. clearStores() deletes the crypto/sync
  // IndexedDB stores, and its `onblocked` handler never resolves — a second
  // open tab holding those stores open makes the await hang, which (in the old
  // order) skipped localStorage.clear() and the redirect, leaving the access
  // token on a shared device. Wiping localStorage up front fail-safes to
  // "token gone" even if the store deletion blocks.
  window.localStorage.clear();
  // Best-effort crypto/sync store wipe, bounded so a blocked deletion can't
  // hang logout — the credential is already gone by here.
  try {
    await Promise.race([
      mx.clearStores(),
      new Promise<void>((resolve) => {
        window.setTimeout(resolve, 3000);
      }),
    ]);
  } catch {
    // ignore — credential already cleared above.
  }
  // Navigate to root instead of reload(): on Android Capacitor the
  // WebViewLocalServer fails to SPA-fallback on URLs with `%3A` in path
  // segments (Matrix room/space ids), so reload of `/<spaceId>/<roomId>/`
  // or `/channels/<spaceId>/...` returns ERR_HTTP_RESPONSE_CODE_FAILURE.
  // Replacing to `/` always resolves to index.html; the Router index
  // loader then redirects to the login page since the session is gone.
  window.location.replace('/');
};

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

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

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

// Boot-error logout: full LOCAL cleanup with no network calls — useful when the
// network is the reason we're stuck (sync error, init/start failure). Wipes
// the native session bridge so CallDeclineReceiver can't resurrect a dead
// token, clears push state so a re-login doesn't double-register, and nukes
// IndexedDB + localStorage. Server-side logout is skipped — the homeserver
// will time out the session naturally.
export const clearLocalSessionAndReload = async () => {
  await clearSessionBridge();
  // Wipe the Tink-encrypted proxy creds + clear the override + tear down the
  // relay on this local-only logout too (proxy_support.md §15, locked).
  await proxy.clearConfig();
  // Same reasoning as the normal logoutClient: kill the WorkManager
  // polling fallback before the reload so it can't fire one more time
  // with the old access_token and surface notifications from the
  // logged-out account.
  await polling.cancel();
  await polling.clearSession();
  clearPusherIds();
  setPushEnabled(false);

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

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