import { logger } from '@sentry/react';

import { getRetryTimeout, isNetworkError } from 'src/utils/retry';
import wait from 'src/utils/wait';

import { type AppInitializerOptions } from './types';

export type { AppInitializerOptions };

/**
 * Coordinates the app's one-time initialization (e.g. reading tokens, fetching
 * the user profile) with transient-failure retry support.
 *
 * Behavior:
 *   - On a network error *and* valid tokens: keep retrying with incremental backoff.
 *   - On a non-network error *or* no valid tokens: stop, fire `onAuthFailure` once.
 *   - On `abort()` while sleeping in backoff: wake up immediately (wait() resolves).
 *   - Under React StrictMode: two back-to-back mounts both call `run()`; the second
 *     call aborts the first and chains onto its finalization, so no double-booting.
 *
 * Design decisions (do not change without care):
 *   - `runningPromise` is the single source of truth for "is a loop in flight".
 *     No separate status field.
 *   - `authFailureFired` resets only on cold start (else-branch). If the first
 *     StrictMode loop already called `onAuthFailure`, the chained second loop
 *     must not fire it again.
 *   - The `=== currentPromise` guard before nulling `runningPromise` prevents
 *     Mount 1's cleanup from clobbering the promise Mount 2 just assigned.
 *   - `wait()` resolves (not rejects) on abort; the loop checks
 *     `signal.aborted` at the top of each iteration rather than via catch.
 */
export class AppInitializer {
  private runningPromise: Promise<void> | null = null;

  private abortController: AbortController = new AbortController();

  private authFailureFired = false;

  public run = async (
    bootFn: () => Promise<void>,
    options: AppInitializerOptions,
  ): Promise<void> => {
    if (this.runningPromise) {
      // authFailureFired is intentionally NOT reset here — if the first loop
      // already called onAuthFailure, the chained second loop must not call it again.
      this.abort();
      await this.runningPromise;
    } else {
      this.authFailureFired = false;
    }

    this.abortController = new AbortController();
    let retryAttempt = 0;

    const currentPromise = (async () => {
      while (true) {
        if (this.abortController.signal.aborted) {
          return;
        }

        try {
          await bootFn();
          return;
        } catch (error) {
          const hasTokens = options.hasValidTokens();
          const isNetwork = isNetworkError(error);

          logger.error('AppInitializer boot error', {
            module: 'Web',
            component: 'AppInitializer',
            isNetworkError: isNetwork,
            hasValidTokens: hasTokens,
            retryAttempt,
            error: error instanceof Error ? error.message : String(error),
          });

          if (!isNetwork || !hasTokens) {
            if (!this.authFailureFired) {
              this.authFailureFired = true;
              options.onAuthFailure(error);
            }
            return;
          }

          const timeout = getRetryTimeout(retryAttempt);
          if (timeout > 0) {
            await wait(timeout, { signal: this.abortController.signal });
          }
          retryAttempt++;
        }
      }
    })();

    this.runningPromise = currentPromise;
    await currentPromise;
    // Only clear if no newer run() has taken over — prevents Mount 1's cleanup
    // from nulling out the promise that Mount 2 (StrictMode remount) just assigned.
    if (this.runningPromise === currentPromise) {
      this.runningPromise = null;
    }
  };

  public abort = (): void => {
    if (this.runningPromise) {
      this.abortController.abort();
    }
  };
}

export const appInitializer = new AppInitializer();
