/*
 * Copyright (C) 2019 Robert Bosch GmbH Copyright (C) 2019 Robert Bosch Manufacturing Solutions GmbH, Germany. All rights reserved.
 */

import { LogLevel, OpenIdConfiguration, StsConfigHttpLoader } from 'angular-auth-oidc-client';
import { combineLatest } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { asObservable } from '../as-observable';
import { DefaultMacmaAuthModuleConfig, IMacmaAuthModuleConfig } from './macma-auth.options';
import { MacmaStorageService, STORAGE_SCOPE_REDIRECT_QUERY_PARAMETER } from './services/macma-storage.service';
import { OauthService } from './services/oauth.service';

export class AuthOIDCInitializer {
  static loadOIDCConfiguration(
    config: IMacmaAuthModuleConfig,
    oauthService: OauthService,
    storageService: MacmaStorageService,
    baseHref?: string,
  ): StsConfigHttpLoader {
    storageService.initializeStorageScope(config);
    return new StsConfigHttpLoader(
      AuthOIDCInitializer.loadOIDCConfigurationInternal(config, oauthService, storageService, baseHref)
    );
  }

  private static loadOIDCConfigurationInternal(
    config: IMacmaAuthModuleConfig,
    oauthService: OauthService,
    storageService: MacmaStorageService,
    baseHref?: string) {
    baseHref = baseHref || '/';

    const urlParams = new URLSearchParams(window.location.search);
    const queriedRealm = urlParams.get(config.queryParamRealm || 'tenant');

    // try to retrieve remembered realm
    const hasStorage = this.hasStorage();
    let storedRealm = hasStorage ? storageService.read('realm') : null;
    if (storedRealm === 'undefined' || storedRealm === 'null') {
      storedRealm = undefined;
      storageService.remove('realm');
    }

    const redirectUrl = this.getRedirectUrl(urlParams, storageService);

    return combineLatest([asObservable(config.clientId), asObservable(config.defaultRealm), asObservable(config.authServerBaseUrl ?? config.accessControlEndpoint)]).pipe(
      map(([clientId, defaultRealm, authServerBaseUrl]) => {
        // use realm from query parameter over session storage over default configuration
        const desiredRealm = queriedRealm || storedRealm || defaultRealm;

        const configId = `web-core-auth-${clientId}`;

        // Clear persisted tokens etc. when switching tenant/realm
        if (!!storedRealm && desiredRealm !== storedRealm) {
          // see unexported StoragePersistanceService
          if (hasStorage) {
            if (config.debugAuthentication) {
              console.log('clearing other tenant\'s auth state');
            }
            // key equals configid as defined below, see also lib's OidcConfigService.createUniqueIds and ResetAuthDataService.resetAuthorizationData
            storageService.remove(configId);
          }
        }

        // remember realm
        if (hasStorage) {
          storageService.write('realm', desiredRealm);
        }

        if (!desiredRealm) {
          console.error(
            'No tenant configured and none set for authentication. ' +
            'Consider configuring defaultRealm or setting \'' +
            config.queryParamRealm || 'tenant' + '\' query parameter.'
          );
        }
        oauthService.setRealm(desiredRealm);

        const authServerUrl = this.getAuthServerUrl(authServerBaseUrl, desiredRealm);
        const customParamsAuthRequest: Record<string, string> = {};
        if (storageService.storageScope) {
          customParamsAuthRequest[STORAGE_SCOPE_REDIRECT_QUERY_PARAMETER] = storageService.storageScope;
        }

        console.log('CONFIGURING OIDC with auth server ' + authServerUrl);
        return {
          configId: configId, // see above where stored things are cleared e.g. when switching tenants/realms
          authority: authServerUrl,
          autoUserinfo: config.requestUserInfo, // somehow triggers CORS issues with Keycloak 4.5/4.6 and thereby kills silent renew
          // The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified by the iss (issuer) Claim as an audience.
          // The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences not trusted by the Client.
          clientId: clientId,
          responseType: 'code', // 'id_token token' Implicit Flow
          scope: config.scope || DefaultMacmaAuthModuleConfig.scope,
          startCheckSession: false,
          silentRenew: config.automaticTokenRefresh, // silently renew token via hidden iframe as long as session with identity provider exists
          useRefreshToken: config.useRefreshToken,
          ignoreNonceAfterRefresh: true, // makes refresh tokens work with keycloak
          renewTimeBeforeTokenExpiresInSeconds: config.refreshTokenTimeInSeconds,
          silentRenewUrl:
            window.location.protocol + '//' + window.location.host + baseHref + config.authAssetsPath + 'renew.html',
          triggerAuthorizationResultEvent: false, // set to false to enable post login route navigation. this setting has no effect on the auth event stream
          forbiddenRoute: '/forbidden', // HTTP 403
          unauthorizedRoute: '/unauthorized', // HTTP 401
          logLevel:
            config.debugAuthentication || DefaultMacmaAuthModuleConfig.debugAuthentication
              ? LogLevel.Debug
              : LogLevel.Warn,
          // id_token C8: The iat Claim can be used to reject tokens that were issued too far away from the current time,
          // As only server side checks are relevant and telling users to sync their clocks is not great and the security benefit is negligible,
          // iat claim check of the ID token is disabled.
          // (similar to what Auth0 did as well, see https://github.com/damienbod/angular-auth-oidc-client/issues/419 and https://github.com/auth0/auth0-spa-js/issues/320)
          disableIatOffsetValidation: true,

          historyCleanupOff: true,

          redirectUrl,
          postLoginRoute: storageService.read('postLoginRoute') ?? window.location.pathname,
          postLogoutRoute: window.location.pathname,
          postLogoutRedirectUri: window.location.href.split('?')[0],
          storage: storageService,
          customParamsAuthRequest,
        } as OpenIdConfiguration;
      }),
      shareReplay(1),
      map(newConfig =>
        // Workaround: return existing config because checkAuth of OIDC lib always loads config again, thus overwriting the changes made for popup login.
        // OIDC Lib's native popup functionality does not work for external identity providers because windows.opener is null in that case and thus the
        // callback post message does not arrive. The native popup functionality uses an internal checkAuth and passes the current confings instead of getting
        // new the original configs from the loader.
        // Potential risk: lib is used to get an original config frequently, so returning the used/altered config might have side affects in the lib.
        oauthService.getCurrentOpenIdFlowConfiguration() ?? newConfig
      )
    );
  }

  private static hasStorage() {
    return typeof Storage !== 'undefined';
  }

  private static getRedirectUrl(urlParams: URLSearchParams, storageService: MacmaStorageService) {
    const hasStorage = this.hasStorage();

    // must match the one used during login at /auth endpoint
    const defaultRedirectUrl = window.location.href;

    // try to retrieve remembered redirect url to avoid invalid code issues (400 on code exchange for token)
    let storedRedirectUrl = hasStorage ? storageService.read('redirectUrl') : null;
    if (storedRedirectUrl === 'undefined' || storedRedirectUrl === 'null') {
      storedRedirectUrl = undefined;
      storageService.remove('redirectUrl');
    }

    // use redirect from session storage if we have an authorization callback request
    const redirectUrl = urlParams.has('code') ? storedRedirectUrl : defaultRedirectUrl;
    // remember redirect url
    if (hasStorage) {
      storageService.write('redirectUrl', redirectUrl);
    }
    console.log('Using redirect url "' + redirectUrl + '"');
    return redirectUrl;
  }

  private static getAuthServerUrl(authServerBaseUrl: string, desiredRealm: string) {
    if (desiredRealm === null || desiredRealm === undefined) {
      console.log('Using authorization Server ' + authServerBaseUrl + ' without any realm');
      return authServerBaseUrl;
    } else {
      console.log('Using authorization Server ' + authServerBaseUrl + ' with realm ' + desiredRealm);
      return authServerBaseUrl + '/auth/realms/' + desiredRealm;
    }
  }
}
