import { Injectable, inject } from 'injection-js';
import { type Observable, ReplaySubject, forkJoin, of, throwError } from 'rxjs';
import { catchError, map, share, switchMap } from 'rxjs/operators';
import { AuthStateService } from '../auth-state/auth-state.service';
import { ConfigurationService } from '../config/config.service';
import type { OpenIdConfiguration } from '../config/openid-configuration';
import type { CallbackContext } from '../flows/callback-context';
import { FlowsDataService } from '../flows/flows-data.service';
import { ResetAuthDataService } from '../flows/reset-auth-data.service';
import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.service';
import { LoggerService } from '../logging/logger.service';
import { EventTypes } from '../public-events/event-types';
import { PublicEventsService } from '../public-events/public-events.service';
import { StoragePersistenceService } from '../storage/storage-persistence.service';
import { UserService } from '../user-data/user.service';
import { FlowHelper } from '../utils/flowHelper/flow-helper.service';
import { IntervalService } from './interval.service';
import { RefreshSessionRefreshTokenService } from './refresh-session-refresh-token.service';

@Injectable()
export class PeriodicallyTokenCheckService {
  private readonly resetAuthDataService = inject(ResetAuthDataService);

  private readonly flowHelper = inject(FlowHelper);

  private readonly flowsDataService = inject(FlowsDataService);

  private readonly loggerService = inject(LoggerService);

  private readonly userService = inject(UserService);

  private readonly authStateService = inject(AuthStateService);

  private readonly refreshSessionIframeService = inject(
    RefreshSessionIframeService
  );

  private readonly refreshSessionRefreshTokenService = inject(
    RefreshSessionRefreshTokenService
  );

  private readonly intervalService = inject(IntervalService);

  private readonly storagePersistenceService = inject(
    StoragePersistenceService
  );

  private readonly publicEventsService = inject(PublicEventsService);

  private readonly configurationService = inject(ConfigurationService);

  startTokenValidationPeriodically(
    allConfigs: OpenIdConfiguration[],
    currentConfig: OpenIdConfiguration
  ): Observable<undefined> {
    const configsWithSilentRenewEnabled =
      this.getConfigsWithSilentRenewEnabled(allConfigs);

    if (configsWithSilentRenewEnabled.length <= 0) {
      return of(undefined);
    }

    if (this.intervalService.isTokenValidationRunning()) {
      return of(undefined);
    }

    const refreshTimeInSeconds = this.getSmallestRefreshTimeFromConfigs(
      configsWithSilentRenewEnabled
    );
    const periodicallyCheck$ = this.intervalService
      .startPeriodicTokenCheck(refreshTimeInSeconds)
      .pipe(
        switchMap(() => {
          const objectWithConfigIdsAndRefreshEvent: {
            [id: string]: Observable<boolean | CallbackContext | null>;
          } = {};

          for (const config of configsWithSilentRenewEnabled) {
            const identifier = config.configId as string;
            const refreshEvent = this.getRefreshEvent(config, allConfigs);

            objectWithConfigIdsAndRefreshEvent[identifier] = refreshEvent;
          }

          return forkJoin(objectWithConfigIdsAndRefreshEvent);
        })
      );

    const o$ = periodicallyCheck$.pipe(
      catchError((error) => {
        this.loggerService.logError(
          currentConfig,
          'silent renew failed!',
          error
        );
        return throwError(() => error);
      }),
      map((objectWithConfigIds) => {
        for (const [configId, _] of Object.entries(objectWithConfigIds)) {
          this.configurationService
            .getOpenIDConfiguration(configId)
            .subscribe((config) => {
              this.loggerService.logDebug(
                config,
                'silent renew, periodic check finished!'
              );

              if (
                this.flowHelper.isCurrentFlowCodeFlowWithRefreshTokens(config)
              ) {
                this.flowsDataService.resetSilentRenewRunning(config);
              }
            });
          return undefined;
        }
      }),
      share({
        connector: () => new ReplaySubject(1),
        resetOnError: false,
        resetOnComplete: false,
        resetOnRefCountZero: false,
      })
    );

    this.intervalService.runTokenValidationRunning = o$.subscribe({});

    return o$;
  }

  private getRefreshEvent(
    config: OpenIdConfiguration,
    allConfigs: OpenIdConfiguration[]
  ): Observable<boolean | CallbackContext | null> {
    const shouldStartRefreshEvent =
      this.shouldStartPeriodicallyCheckForConfig(config);

    if (!shouldStartRefreshEvent) {
      return of(null);
    }

    const refreshEvent$ = this.createRefreshEventForConfig(config, allConfigs);

    this.publicEventsService.fireEvent(EventTypes.SilentRenewStarted);

    return refreshEvent$.pipe(
      catchError((error) => {
        this.loggerService.logError(config, 'silent renew failed!', error);
        this.publicEventsService.fireEvent(EventTypes.SilentRenewFailed, error);
        this.flowsDataService.resetSilentRenewRunning(config);

        return throwError(() => new Error(error));
      })
    );
  }

  private getSmallestRefreshTimeFromConfigs(
    configsWithSilentRenewEnabled: OpenIdConfiguration[]
  ): number {
    const result = configsWithSilentRenewEnabled.reduce((prev, curr) =>
      (prev.tokenRefreshInSeconds ?? 0) < (curr.tokenRefreshInSeconds ?? 0)
        ? prev
        : curr
    );

    return result.tokenRefreshInSeconds ?? 0;
  }

  private getConfigsWithSilentRenewEnabled(
    allConfigs: OpenIdConfiguration[]
  ): OpenIdConfiguration[] {
    return allConfigs.filter((x) => x.silentRenew);
  }

  private createRefreshEventForConfig(
    configuration: OpenIdConfiguration,
    allConfigs: OpenIdConfiguration[]
  ): Observable<boolean | CallbackContext | null> {
    this.loggerService.logDebug(configuration, 'starting silent renew...');

    return this.configurationService
      .getOpenIDConfiguration(configuration.configId)
      .pipe(
        switchMap((config) => {
          if (!config?.silentRenew) {
            this.resetAuthDataService.resetAuthorizationData(
              config,
              allConfigs
            );

            return of(null);
          }

          this.flowsDataService.setSilentRenewRunning(config);

          if (this.flowHelper.isCurrentFlowCodeFlowWithRefreshTokens(config)) {
            // Retrieve Dynamically Set Custom Params for refresh body
            const customParamsRefresh: {
              [key: string]: string | number | boolean;
            } =
              this.storagePersistenceService.read(
                'storageCustomParamsRefresh',
                config
              ) || {};

            const { customParamsRefreshTokenRequest } = config;

            const mergedParams = {
              ...customParamsRefreshTokenRequest,
              ...customParamsRefresh,
            };

            // Refresh Session using Refresh tokens
            return this.refreshSessionRefreshTokenService.refreshSessionWithRefreshTokens(
              config,
              allConfigs,
              mergedParams
            );
          }

          // Retrieve Dynamically Set Custom Params
          const customParams: { [key: string]: string | number | boolean } =
            this.storagePersistenceService.read(
              'storageCustomParamsAuthRequest',
              config
            );

          return this.refreshSessionIframeService.refreshSessionWithIframe(
            config,
            allConfigs,
            customParams
          );
        })
      );
  }

  private shouldStartPeriodicallyCheckForConfig(
    config: OpenIdConfiguration
  ): boolean {
    const idToken = this.authStateService.getIdToken(config);
    const isSilentRenewRunning =
      this.flowsDataService.isSilentRenewRunning(config);
    const isCodeFlowInProgress =
      this.flowsDataService.isCodeFlowInProgress(config);
    const userDataFromStore = this.userService.getUserDataFromStore(config);

    this.loggerService.logDebug(
      config,
      `Checking: silentRenewRunning: ${isSilentRenewRunning}, isCodeFlowInProgress: ${isCodeFlowInProgress} - has idToken: ${!!idToken} - has userData: ${!!userDataFromStore}`
    );

    const shouldBeExecuted =
      !!userDataFromStore &&
      !isSilentRenewRunning &&
      !!idToken &&
      !isCodeFlowInProgress;

    if (!shouldBeExecuted) {
      return false;
    }

    const idTokenExpired =
      this.authStateService.hasIdTokenExpiredAndRenewCheckIsEnabled(config);
    const accessTokenExpired =
      this.authStateService.hasAccessTokenExpiredIfExpiryExists(config);

    return idTokenExpired || accessTokenExpired;
  }
}
