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

import { APP_BASE_HREF } from '@angular/common';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { APP_INITIALIZER, ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
import { AbstractSecurityStorage, AuthModule as OidcAuthModule, EventTypes, OidcSecurityService, PublicEventsService, StsConfigLoader } from 'angular-auth-oidc-client';
import { delay, fromEvent, merge, Observable, of, switchMap } from 'rxjs';
import { distinctUntilChanged, filter, first, map, tap } from 'rxjs/operators';
import { AUTH_OPTIONS } from '../auth-options.token';
import { AuthInterceptor } from '../auth.interceptor';
import { IAuthModuleConfig } from '../auth.options';
import { AuthenticationService } from '../services/authentication.service';
import { AuthorizationService } from '../services/authorization.service';
import { AuthOIDCInitializer } from './auth-oidc.initializer';
import { DefaultMacmaAuthModuleConfig, IMacmaAuthModuleConfig, IMacmaAuthModuleOptions } from './macma-auth.options';
import { MacmaAuthenticationService } from './services/macma-authentication.service';
import { MacmaAuthorizationService } from './services/macma-authorization.service';
import { MacmaStorageService } from './services/macma-storage.service';
import { OauthService } from './services/oauth.service';

export const httpLoaderFactory = (
  config: IAuthModuleConfig,
  oauthService: OauthService,
  storageService: MacmaStorageService,
  baseHref?: string,
) => {
  return AuthOIDCInitializer.loadOIDCConfiguration(config, oauthService, storageService, baseHref);
};

@NgModule({
  imports: [
    HttpClientModule,
    OidcAuthModule.forRoot({
      loader: {
        provide: StsConfigLoader,
        useFactory: httpLoaderFactory,
        deps: [AUTH_OPTIONS, OauthService, AbstractSecurityStorage, [new Optional(), APP_BASE_HREF]],
      },
    }),
  ],
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: autoLoginFactory,
      deps: [PublicEventsService, OidcSecurityService, OauthService, AUTH_OPTIONS],
      multi: true,
    },
    {
      provide: AbstractSecurityStorage,
      useClass: MacmaStorageService,
    }
  ],
})
export class MacmaAuthModule {
  constructor(@Optional() @SkipSelf() parentModule: MacmaAuthModule, private oidcSecurityService: OidcSecurityService) {
    if (parentModule) {
      throw new Error(
        "MacmaAuthModule is already loaded. It should only be imported in your application's main module."
      );
    }

    merge(
      // Authorization callback handling for Iframes and Popups
      fromEvent(window, 'oidc-code-message').pipe(
        tap((event: CustomEvent) => console.log('Received auth callback via event/message', event)),
        map((event: CustomEvent) => event.detail)
      ),

      // Authorization callback handling for popups when external identity provider (= redirect to different domain) is involved,
      // and thus window.opener is null inside popup, making events unusable
      fromEvent(window, 'storage').pipe(
        filter((event: StorageEvent) => event.key === 'oidc-code-message'),
        filter((event: StorageEvent) => !!event.newValue),
        tap((event: StorageEvent) => console.log('Received auth callback via storage', event)),
        // check if still in storage, otherwise another window already handled it
        filter((event: StorageEvent) => {
          const isStillInStorage = !!localStorage.getItem(event.key);
          if (!isStillInStorage) {
            console.log('Another window has handled the auth callback already. Processing stopped.');
          }
          return isStillInStorage;
        }),
        // cleanup of stored value
        tap((event: StorageEvent) => localStorage.removeItem(event.key)),
        map((event: StorageEvent) => event.newValue)
      )
    )
      .pipe(
        // make sure we only try code-exchange once per login callback
        distinctUntilChanged()
      )
      .subscribe((callbackUrl: string) => {
        console.log('Received authorization callback.', callbackUrl);
        this.oidcSecurityService
          .checkAuth(callbackUrl)
          .pipe(first())
          .subscribe((result) => console.log('Auth result:', result));
      });

    oidcSecurityService.stsCallback$;
  }

  static forRoot(options: IMacmaAuthModuleOptions): ModuleWithProviders<MacmaAuthModule> {
    return {
      ngModule: MacmaAuthModule,
      providers: [
        options.authOptionsProvider || {
          provide: AUTH_OPTIONS,
          useValue: Object.assign({}, DefaultMacmaAuthModuleConfig, options.config),
        },
        OauthService,
        {
          provide: AuthenticationService,
          useClass: MacmaAuthenticationService,
        },
        {
          provide: HTTP_INTERCEPTORS,
          useClass: AuthInterceptor,
          deps: [AUTH_OPTIONS, AuthenticationService],
          multi: true,
        },
        {
          provide: AuthorizationService,
          useClass: MacmaAuthorizationService,
        },
      ],
    };
  }
}
function autoLoginFactory(
  publicEventsService: PublicEventsService,
  oidcSecurityService: OidcSecurityService,
  oauthService: OauthService,
  config: IMacmaAuthModuleConfig
): () => Observable<any> {
  return () =>
    oidcSecurityService.preloadAuthWellKnownDocument().pipe(
      switchMap(() => publicEventsService.registerForEvents()),
      filter((event) => event.type === EventTypes.ConfigLoaded),
      first(),
      //switchMap((oidcConfig) => oidcConfigService.withConfig(oidcConfig)),
      switchMap(() => {
        const urlParams = new URLSearchParams(window.location.search);
        if (urlParams.has('code')) {
          if (config.debugAuthentication) {
            console.log('Got a login callback. Exchanging code for token.');
          }
          return oidcSecurityService.checkAuth();
          // if not login callback and user is not authenticated, trigger check for SSO
          // auth result is persisted, but without the following code, the lib does not initialize it's authenticated state and refresh check correctly
        } else if (!urlParams.has('code') && !urlParams.has('error')) {
          // similar to AutoLoginGuard https://github.com/damienbod/angular-auth-oidc-client/blob/main/projects/angular-auth-oidc-client/src/lib/auto-login/auto-login.guard.ts
          if (config.debugAuthentication) {
            console.log('Checking login status');
          }
          return oidcSecurityService.isAuthenticated$.pipe(
            first(),
            switchMap((result) =>
              result.isAuthenticated
                ? of(true)
                : oidcSecurityService.checkAuth().pipe(map((loginResponse) => loginResponse.isAuthenticated))
            ),
            first(),
            switchMap((isAuthenticated: boolean) => {
              console.log('Is authenticated: ' + isAuthenticated);
              if (!isAuthenticated && config.autoLogin) {
                if (config.debugAuthentication) {
                  console.log('Triggering autologin');
                }
                oauthService.login(oidcSecurityService, false);
                // login is done via redirects, therefore this initializer should wait for login flow to be triggerd two avoid flashing bootstrapping
                return of(false).pipe(delay(50));
              } else {
                return of(true);
              }
            })
          );
        }
        return of(void 0);
      })
    );
}
