import { HttpHeaders } from '@ngify/http';
import { Injectable, inject } from 'injection-js';
import { type Observable, of, throwError, timer } from 'rxjs';
import { catchError, mergeMap, retryWhen, switchMap } from 'rxjs/operators';
import { DataService } from '../../api/data.service';
import type { OpenIdConfiguration } from '../../config/openid-configuration';
import { LoggerService } from '../../logging/logger.service';
import { StoragePersistenceService } from '../../storage/storage-persistence.service';
import { UrlService } from '../../utils/url/url.service';
import { TokenValidationService } from '../../validation/token-validation.service';
import type { AuthResult, CallbackContext } from '../callback-context';
import { FlowsDataService } from '../flows-data.service';
import { isNetworkError } from './error-helper';

@Injectable()
export class CodeFlowCallbackHandlerService {
  private readonly urlService = inject(UrlService);

  private readonly loggerService = inject(LoggerService);

  private readonly tokenValidationService = inject(TokenValidationService);

  private readonly flowsDataService = inject(FlowsDataService);

  private readonly storagePersistenceService = inject(
    StoragePersistenceService
  );

  private readonly dataService = inject(DataService);

  // STEP 1 Code Flow
  codeFlowCallback(
    urlToCheck: string,
    config: OpenIdConfiguration
  ): Observable<CallbackContext> {
    const code = this.urlService.getUrlParameter(urlToCheck, 'code');
    const state = this.urlService.getUrlParameter(urlToCheck, 'state');
    const sessionState = this.urlService.getUrlParameter(
      urlToCheck,
      'session_state'
    );

    if (!state) {
      this.loggerService.logDebug(config, 'no state in url');

      return throwError(() => new Error('no state in url'));
    }

    if (!code) {
      this.loggerService.logDebug(config, 'no code in url');

      return throwError(() => new Error('no code in url'));
    }

    this.loggerService.logDebug(
      config,
      'running validation for callback',
      urlToCheck
    );

    const initialCallbackContext: CallbackContext = {
      code,
      refreshToken: '',
      state,
      sessionState,
      authResult: null,
      isRenewProcess: false,
      jwtKeys: null,
      validationResult: null,
      existingIdToken: null,
    };

    return of(initialCallbackContext);
  }

  // STEP 2 Code Flow //  Code Flow Silent Renew starts here
  codeFlowCodeRequest(
    callbackContext: CallbackContext,
    config: OpenIdConfiguration
  ): Observable<CallbackContext> {
    const authStateControl = this.flowsDataService.getAuthStateControl(config);
    const isStateCorrect =
      this.tokenValidationService.validateStateFromHashCallback(
        callbackContext.state,
        authStateControl,
        config
      );

    if (!isStateCorrect) {
      return throwError(() => new Error('codeFlowCodeRequest incorrect state'));
    }

    const authWellknownEndpoints = this.storagePersistenceService.read(
      'authWellKnownEndPoints',
      config
    );
    const tokenEndpoint = authWellknownEndpoints?.tokenEndpoint;

    if (!tokenEndpoint) {
      return throwError(() => new Error('Token Endpoint not defined'));
    }

    let headers: HttpHeaders = new HttpHeaders();

    headers = headers.set('Content-Type', 'application/x-www-form-urlencoded');

    const bodyForCodeFlow = this.urlService.createBodyForCodeFlowCodeRequest(
      callbackContext.code,
      config,
      config?.customParamsCodeRequest
    );

    return this.dataService
      .post(tokenEndpoint, bodyForCodeFlow, config, headers)
      .pipe(
        switchMap((response) => {
          if (response) {
            const authResult: AuthResult = {
              ...(response as any),
              state: callbackContext.state,
              session_state: callbackContext.sessionState,
            };

            callbackContext.authResult = authResult;
          }

          return of(callbackContext);
        }),
        retryWhen((error) => this.handleRefreshRetry(error, config)),
        catchError((error) => {
          const { authority } = config;
          const errorMessage = `OidcService code request ${authority}`;

          this.loggerService.logError(config, errorMessage, error);

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

  private handleRefreshRetry(
    errors: Observable<unknown>,
    config: OpenIdConfiguration
  ): Observable<unknown> {
    return errors.pipe(
      mergeMap((error) => {
        // retry token refresh if there is no internet connection
        if (isNetworkError(error)) {
          const { authority, refreshTokenRetryInSeconds } = config;
          const errorMessage = `OidcService code request ${authority} - no internet connection`;

          this.loggerService.logWarning(config, errorMessage, error);

          return timer((refreshTokenRetryInSeconds ?? 0) * 1000);
        }

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