/** Browser tabs share credentials only while a page session is available. */
interface AuthSnapshot {
  revision: string;
  value: null | string;
}

interface ValueStorage {
  getItem: (key: string) => null | string;
  setItem: (key: string, value: string) => void;
}

interface SessionAuthOptions {
  channel?: BroadcastChannel;
  events: Window;
  key: string;
  onRemoteChange: () => void;
  persistent: ValueStorage;
  session: ValueStorage;
}

function tokenOf(value: null | string): null | string {
  if (!value) return null;
  try {
    return JSON.parse(value).accessToken || null;
  } catch {
    return null;
  }
}

export function createSessionAuthStorage(options: SessionAuthOptions) {
  const { channel, events, key, onRemoteChange, persistent, session } = options;
  // Only an opaque revision is persistent; it cannot restore authentication.
  const revisionKey = `${key}-revision`;
  const snapshotKey = `${key}-session`;
  const revision = () => persistent.getItem(revisionKey) || '';
  let current: AuthSnapshot = { revision: revision(), value: null };
  try {
    const saved = JSON.parse(session.getItem(snapshotKey) || 'null');
    if (saved && saved.revision === current.revision && typeof saved.value === 'string') current = saved;
  } catch {
    // Unreadable session data must never prevent the login page from opening.
  }
  let started = false;
  let disposed = false;
  let timer: ReturnType<typeof setTimeout> | undefined;
  let finishDiscovery: (() => void) | undefined;

  function save(snapshot: AuthSnapshot) {
    current = snapshot;
    session.setItem(snapshotKey, JSON.stringify(current));
  }

  function requestSession() {
    channel?.postMessage({ type: 'request' });
  }

  function reconcile() {
    if (disposed || current.revision === revision()) return;
    const hadToken = !!tokenOf(current.value);
    save({ revision: revision(), value: null });
    if (started && hadToken) onRemoteChange();
  }

  function checkRevision() {
    if (current.revision === revision()) return;
    requestSession();
    // Let the peer's snapshot arrive before reinitializing the application.
    clearTimeout(timer);
    timer = setTimeout(reconcile, 250);
  }

  function receive(event: MessageEvent) {
    const message = event.data;
    if (message?.type === 'request') {
      if (current.revision === revision() && tokenOf(current.value)) {
        channel?.postMessage({ type: 'snapshot', snapshot: current });
      }
      return;
    }
    const snapshot = message?.snapshot as AuthSnapshot | undefined;
    if (
      message?.type !== 'snapshot' ||
      !snapshot ||
      typeof snapshot.revision !== 'string' ||
      (snapshot.value !== null && typeof snapshot.value !== 'string') ||
      snapshot.revision !== revision()
    )
      return;
    const changed = tokenOf(current.value) !== tokenOf(snapshot.value);
    save(snapshot);
    finishDiscovery?.();
    if (started && changed) onRemoteChange();
  }

  function storageChanged(event: StorageEvent) {
    if (event.key === revisionKey || event.key === null) checkRevision();
  }
  channel?.addEventListener('message', receive);
  events.addEventListener('storage', storageChanged);
  events.addEventListener('pageshow', checkRevision);
  events.addEventListener('focus', checkRevision);
  events.document?.addEventListener('visibilitychange', checkRevision);

  return {
    async start() {
      if ((!tokenOf(current.value) || current.revision !== revision()) && channel) {
        await new Promise<void>((resolve) => {
          const timeout = setTimeout(() => {
            finishDiscovery = undefined;
            resolve();
          }, 350);
          finishDiscovery = () => {
            clearTimeout(timeout);
            finishDiscovery = undefined;
            resolve();
          };
          requestSession();
        });
      }
      reconcile();
      started = true;
    },
    getAccessToken() {
      return current.revision === revision() ? tokenOf(current.value) : null;
    },
    getItem() {
      // A sleeping tab must not hydrate a credential revoked by another tab.
      return current.revision === revision() ? current.value : null;
    },
    setItem(_key: string, value: string) {
      if (current.revision !== revision()) {
        checkRevision();
        return;
      }
      if (value === current.value) return;
      const changed = tokenOf(current.value) !== tokenOf(value);
      let nextRevision = current.revision;
      if (changed) {
        nextRevision = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
        persistent.setItem(revisionKey, nextRevision);
      }
      save({ revision: nextRevision, value });
      if (changed) channel?.postMessage({ type: 'snapshot', snapshot: current });
    },
    dispose() {
      disposed = true;
      clearTimeout(timer);
      finishDiscovery?.();
      channel?.removeEventListener('message', receive);
      channel?.close();
      events.removeEventListener('storage', storageChanged);
      events.removeEventListener('pageshow', checkRevision);
      events.removeEventListener('focus', checkRevision);
      events.document?.removeEventListener('visibilitychange', checkRevision);
    },
  };
}
