import { wsconnect, jwtAuthenticator, type WsSocketFactory } from '@nats-io/nats-core';
import { NatsTransport, type INatsLikeConnection } from './NatsTransport';

export interface ICreateBrowserNatsTransportConfig {
  readonly root?: string;
  /** @deprecated Use `root`. */
  readonly realm?: string;
  readonly wsUrl: string;
  readonly token?: string;
  /**
   * Async function to fetch NATS JWT + NKey seed from /api/nats-jwt.
   * When provided, uses NATS operator/JWT mode instead of plain token auth.
   * A background timer refreshes credentials every 4 minutes (JWT expires in 5).
   *
   * IMPORTANT: jwtAuthenticator callbacks are SYNCHRONOUS. The jwtProvider is called
   * async BEFORE connect, and on a timer — never inside the authenticator callback.
   */
  readonly jwtProvider?: () => Promise<{ jwt: string; seed: string }>;
  /** Static NATS user for user/pass auth (when no JWT/token). */
  readonly user?: string;
  /** Static NATS password for user/pass auth (when no JWT/token). */
  readonly pass?: string;
  readonly debug?: boolean;
  readonly connectionTimeoutMs?: number;
  readonly wsFactory?: WsSocketFactory;
  /**
   * Max reconnect attempts before NATS gives up and the connection closes.
   * Bounded by default so a client-side network/DNS failure (e.g. laptop sleep,
   * Wi-Fi handoff, transient name-resolution failure) cannot turn into an
   * unbounded reconnect storm. -1 means infinite (NOT recommended in the
   * browser — it floods the console and hammers /api/nats-jwt). Default 60:
   * with the backoff schedule below that is ~15 minutes of patient retry
   * before surfacing a clean disconnect the UI can act on.
   */
  readonly maxReconnectAttempts?: number;
}

export async function createBrowserNatsTransport(
  config: ICreateBrowserNatsTransportConfig,
): Promise<NatsTransport> {
  const root = (config.root ?? config.realm ?? '').trim();
  const wsUrl = config.wsUrl.trim();

  if (!root) {
    throw new Error('createBrowserNatsTransport: root is required');
  }

  if (!wsUrl) {
    throw new Error('createBrowserNatsTransport: wsUrl is required');
  }

  // Holder (not a bare variable) so disconnect() always cancels the CURRENT
  // pending timer even after the self-rescheduling refresh has reassigned it.
  const jwtRefreshTimer: { id: ReturnType<typeof setTimeout> | undefined } = { id: undefined };

  try {
    // Build auth options — either JWT or plain token
    let authOpts: Record<string, unknown> = {};

    if (config.jwtProvider) {
      // Pre-fetch initial credentials (async OK here — before wsconnect)
      let currentJwt: string;
      let currentSeed: Uint8Array;

      const refresh = async () => {
        const creds = await config.jwtProvider!();
        // Zero old seed before replacing (defense-in-depth)
        if (currentSeed) currentSeed.fill(0);
        currentJwt = creds.jwt;
        currentSeed = new TextEncoder().encode(creds.seed);
      };

      await refresh();

      // Self-rescheduling refresh instead of a fixed setInterval. The JWT
      // expires in 5 min, so the steady-state cadence is 4 min. On FAILURE
      // (e.g. /api/nats-jwt unreachable because the client's name resolution
      // is failing), a plain setInterval would keep firing every 4 min AND
      // every failed attempt compounds with the NATS reconnect loop hammering
      // the same dead endpoint. Instead we retry with bounded exponential
      // backoff (10s → 80s, capped) so one network blip cannot become a flood.
      const REFRESH_OK_MS = 4 * 60 * 1000;
      const REFRESH_BACKOFF_BASE_MS = 10 * 1000;
      const REFRESH_BACKOFF_MAX_MS = 80 * 1000;
      let refreshFailures = 0;
      const scheduleRefresh = (delayMs: number) => {
        jwtRefreshTimer.id = setTimeout(() => {
          refresh()
            .then(() => {
              refreshFailures = 0;
              scheduleRefresh(REFRESH_OK_MS);
            })
            .catch((e) => {
              refreshFailures += 1;
              const backoff = Math.min(
                REFRESH_BACKOFF_BASE_MS * 2 ** (refreshFailures - 1),
                REFRESH_BACKOFF_MAX_MS,
              );
              // eslint-disable-next-line no-console -- kernel browser transport has no actor context for background JWT refresh failures
              console.warn(
                `[matrix] NATS JWT refresh failed (attempt ${refreshFailures}), retrying in ${Math.round(backoff / 1000)}s:`,
                e,
              );
              scheduleRefresh(backoff);
            });
        }, delayMs);
      };
      scheduleRefresh(REFRESH_OK_MS);

      // jwtAuthenticator takes SYNCHRONOUS closures that read the latest cached values
      authOpts = {
        authenticator: jwtAuthenticator(
          () => currentJwt,
          () => currentSeed,
        ),
      };
    } else if (config.user && config.pass) {
      authOpts = { user: config.user, pass: config.pass };
    } else if (config.token) {
      authOpts = { token: config.token };
    }

    // Bounded reconnect with exponential backoff + jitter. The previous
    // `maxReconnectAttempts: -1` retried forever with a fixed short delay, so a
    // client-side name-resolution failure (laptop sleep, Wi-Fi handoff, the
    // `local.` mDNS-collision gotcha) produced an unbounded WebSocket-failure
    // flood. We now back off 1s → 16s (capped) and give up after a bounded
    // number of attempts, surfacing a clean disconnect the UI can act on
    // instead of hammering a dead endpoint.
    const RECONNECT_BASE_MS = 1000;
    const RECONNECT_MAX_MS = 16 * 1000;
    let reconnectTick = 0;
    const connection = await wsconnect({
      servers: [wsUrl],
      name: `matrix-browser-${root}`,
      inboxPrefix: `${root}.$reply`,
      timeout: config.connectionTimeoutMs ?? 5000,
      reconnect: true,
      maxReconnectAttempts: config.maxReconnectAttempts ?? 60,
      reconnectDelayHandler: () => {
        const delay = Math.min(RECONNECT_BASE_MS * 2 ** reconnectTick, RECONNECT_MAX_MS);
        reconnectTick += 1;
        return delay;
      },
      reconnectJitter: 500,
      wsFactory: config.wsFactory,
      ...authOpts,
    });

    // Reset the backoff tick once a reconnect actually succeeds, so the NEXT
    // outage starts patient again instead of staying pinned at the max delay.
    void (async () => {
      for await (const status of (connection as { status: () => AsyncIterable<{ type: string }> }).status()) {
        if (status.type === 'reconnect') reconnectTick = 0;
      }
    })();

    const transport = new NatsTransport(connection as unknown as INatsLikeConnection, {
      root,
      debug: config.debug ?? false,
    });

    // Attach JWT refresh timer holder so disconnect() can clear the live timer.
    (transport as unknown as { _jwtRefreshTimer: { id: ReturnType<typeof setTimeout> | undefined } })._jwtRefreshTimer = jwtRefreshTimer;

    return transport;
  } catch (error) {
    if (jwtRefreshTimer.id !== undefined) {
      clearTimeout(jwtRefreshTimer.id);
    }
    const message = error instanceof Error ? error.message : String(error);
    throw new Error(`createBrowserNatsTransport: failed to connect WebSocket at ${wsUrl}: ${message}`);
  }
}
