/*
 * Copyright (C) 2022 StApps
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */

import {
  StorageBackend,
  BasicQueryStringUtils,
  DefaultCrypto,
  AuthorizationServiceConfiguration,
  AuthorizationRequest,
  StringMap,
  AuthorizationError,
  AuthorizationErrorJson,
  BUILT_IN_PARAMETERS,
} from '@openid/appauth';
import {Browser} from 'ionic-appauth';
import {PAIAAuthorizationNotifier} from './paia-authorization-notifier';
import {PAIAAuthorizationRequestResponse} from './authorization-request-response';
import {PAIAAuthorizationResponse, PAIAAuthorizationResponseJson} from './paia-authorization-response';

/** key for authorization request. */
const authorizationRequestKey = (handle: string) => {
  return `${handle}_appauth_authorization_request`;
};

/** key in local storage which represents the current authorization request. */
const AUTHORIZATION_REQUEST_HANDLE_KEY = 'appauth_current_authorization_request';
export const AUTHORIZATION_RESPONSE_KEY = 'auth_response';

export class PAIAAuthorizationRequestHandler {
  notifier: PAIAAuthorizationNotifier;

  constructor(
    private browser: Browser,
    private storage: StorageBackend,
    public utils = new BasicQueryStringUtils(),
    protected crypto = new Crypto(),
    private generateRandom = new DefaultCrypto(),
  ) {}

  public async performAuthorizationRequest(
    configuration: AuthorizationServiceConfiguration,
    request: AuthorizationRequest,
  ): Promise<void> {
    const handle = this.generateRandom.generateRandom(10);
    await this.storage.setItem(AUTHORIZATION_REQUEST_HANDLE_KEY, handle);
    await this.storage.setItem(authorizationRequestKey(handle), JSON.stringify(await request.toJson()));
    const url = this.buildRequestUrl(configuration, request);
    const returnedUrl: string | undefined = await this.browser.showWindow(url, request.redirectUri);

    // callback may come from showWindow or via another method
    if (returnedUrl !== undefined) {
      await this.storage.setItem(AUTHORIZATION_RESPONSE_KEY, returnedUrl);
      await this.completeAuthorizationRequestIfPossible();
    }
  }

  protected async completeAuthorizationRequest(): Promise<PAIAAuthorizationRequestResponse> {
    const handle = await this.storage.getItem(AUTHORIZATION_REQUEST_HANDLE_KEY);

    if (!handle) {
      throw new Error('Handle Not Available');
    }

    const request: AuthorizationRequest = this.getAuthorizationRequest(
      await this.storage.getItem(authorizationRequestKey(handle)),
    );
    const queryParameters = this.getQueryParams(await this.storage.getItem(AUTHORIZATION_RESPONSE_KEY));
    void this.removeItemsFromStorage(handle);

    const state: string | undefined = queryParameters['state'];
    const error: string | undefined = queryParameters['error'];

    if (state !== request.state) {
      throw new Error('State Does Not Match');
    }

    return <PAIAAuthorizationRequestResponse>{
      request: request, // request
      response: error ? undefined : this.getAuthorizationResponse(queryParameters),
      error: error ? this.getAuthorizationError(queryParameters) : undefined,
    };
  }

  private getAuthorizationRequest(authRequest: string | null): AuthorizationRequest {
    if (authRequest == undefined) {
      throw new Error('No Auth Request Available');
    }

    return new AuthorizationRequest(JSON.parse(authRequest));
  }

  private getAuthorizationError(queryParameters: StringMap): AuthorizationError {
    const authorizationErrorJSON: AuthorizationErrorJson = {
      error: queryParameters['error'],
      error_description: queryParameters['error_description'],
      error_uri: undefined,
      state: queryParameters['state'],
    };
    return new AuthorizationError(authorizationErrorJSON);
  }

  private getAuthorizationResponse(queryParameters: StringMap): PAIAAuthorizationResponse {
    const authorizationResponseJSON: PAIAAuthorizationResponseJson = {
      code: queryParameters['code'],
      patron: queryParameters['patron'],
      // TODO: currently PAIA is not providing state
      state: queryParameters['state'] ?? '',
    };
    return new PAIAAuthorizationResponse(authorizationResponseJSON);
  }

  private async removeItemsFromStorage(handle: string): Promise<void> {
    await this.storage.removeItem(AUTHORIZATION_REQUEST_HANDLE_KEY);
    await this.storage.removeItem(authorizationRequestKey(handle));
    await this.storage.removeItem(AUTHORIZATION_RESPONSE_KEY);
  }

  private getQueryParams(authResponse: string | null): StringMap {
    if (authResponse == undefined) {
      return {};
    } else {
      const querySide: string = authResponse.split('#')[0];
      const parts: string[] = querySide.split('?');
      if (parts.length !== 2) throw new Error('Invalid auth response string');
      const hash = parts[1];
      return this.utils.parseQueryString(hash);
    }
  }

  setAuthorizationNotifier(notifier: PAIAAuthorizationNotifier): PAIAAuthorizationRequestHandler {
    this.notifier = notifier;
    return this;
  }

  completeAuthorizationRequestIfPossible(): Promise<void> {
    // call complete authorization if possible to see there might
    // be a response that needs to be delivered.
    console.log(`Checking to see if there is an authorization response to be delivered.`);
    if (!this.notifier) {
      console.log(`Notifier is not present on AuthorizationRequest handler.
            No delivery of result will be possible`);
    }
    return this.completeAuthorizationRequest().then(result => {
      if (!result) {
        console.log(`No result is available yet.`);
      }
      if (result && this.notifier) {
        this.notifier.onAuthorizationComplete(result.request, result.response, result.error);
      }
    });
  }

  /**
   * A utility method to be able to build the authorization request URL.
   */
  protected buildRequestUrl(configuration: AuthorizationServiceConfiguration, request: AuthorizationRequest) {
    // build the query string
    // coerce to any type for convenience
    const requestMap: StringMap = {
      redirect_uri: request.redirectUri,
      client_id: request.clientId,
      response_type: request.responseType,
      state: request.state,
      scope: request.scope,
    };

    // copy over extras
    if (request.extras) {
      for (const extra in request.extras) {
        if (
          request.extras.hasOwnProperty(extra) && // check before inserting to requestMap
          !BUILT_IN_PARAMETERS.includes(extra)
        ) {
          requestMap[extra] = request.extras[extra];
        }
      }
    }

    const query = this.utils.stringify(requestMap);
    const baseUrl = configuration.authorizationEndpoint;

    // required encoding (PAIA specific)
    return `${baseUrl}?${encodeURIComponent(query)}`;
  }
}
