/* eslint-disable @typescript-eslint/no-non-null-assertion */
/*!
 * Copyright (c) 2015-present, Okta, Inc. and/or its affiliates. All rights reserved.
 * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
 *
 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and limitations under the License.
 *
 */

/* eslint-disable complexity */
import { isString, clone, isAbsoluteUrl, removeNils } from '../util';
import { STATE_TOKEN_KEY_NAME, DEFAULT_CACHE_DURATION } from '../constants';
import {
  OktaAuthHttpInterface,
  RequestOptions,
  FetchOptions,
  RequestData,
  HttpResponse
} from './types';
import { AuthApiError, OAuthError, AuthSdkError, APIError } from '../errors';

type InsufficientAuthenticationError = {
  error: string;
  // eslint-disable-next-line camelcase
  error_description: string;
  // eslint-disable-next-line camelcase
  max_age: string;
  // eslint-disable-next-line camelcase
  acr_values: string;
};

const parseInsufficientAuthenticationError = (
  header: string
): InsufficientAuthenticationError => {
  if (!header) {
    throw new AuthSdkError('Missing header string');
  }

  return header
    .split(',')
    .map(part => part.trim())
    .map(part => part.split('='))
    .reduce((acc, curr) => {
      // unwrap quotes from value
      acc[curr[0]] = curr[1].replace(/^"(.*)"$/, '$1');
      return acc;
    }, {}) as InsufficientAuthenticationError;
};

const formatError = (sdk: OktaAuthHttpInterface, resp: HttpResponse): AuthApiError | OAuthError => {
  let err: AuthApiError | OAuthError;
  let serverErr: Record<string, any> = {};
  if (resp.responseText && isString(resp.responseText)) {
    try {
      serverErr = JSON.parse(resp.responseText);
    } catch (e) {
      serverErr = {
        errorSummary: 'Unknown error'
      };
    }
  }

  if (resp.status >= 500) {
    serverErr.errorSummary = 'Unknown error';
  }

  if (sdk.options.transformErrorXHR) {
    resp = sdk.options.transformErrorXHR(clone(resp));
  }

  if (serverErr.error && serverErr.error_description) {
    err = new OAuthError(serverErr.error, serverErr.error_description);
  } else {
    err = new AuthApiError(serverErr as APIError, resp);
  }

  if (resp?.status === 403 && !!resp?.headers?.['www-authenticate']) {
    const {
      error,
      // eslint-disable-next-line camelcase
      error_description,
      // eslint-disable-next-line camelcase
      max_age,
      // eslint-disable-next-line camelcase
      acr_values
    } = parseInsufficientAuthenticationError(resp?.headers?.['www-authenticate']);
    if (error === 'insufficient_authentication_context') {
      err = new AuthApiError(
        {
          errorSummary: error,
          // eslint-disable-next-line camelcase
          errorCauses: [{ errorSummary: error_description }]
        },
        resp,
        {
          // eslint-disable-next-line camelcase
          max_age: +max_age,
          // eslint-disable-next-line camelcase
          ...(acr_values && { acr_values })
        }
      );
    }
  }

  return err;
};

export function httpRequest<T = any>(sdk: OktaAuthHttpInterface, options: RequestOptions): Promise<T> {
  options = options || {};

  if (sdk.options.httpRequestInterceptors) {
    for (const interceptor of sdk.options.httpRequestInterceptors) {
      interceptor(options);
    }
  }

  var url = options.url,
      method = options.method,
      args = options.args,
      saveAuthnState = options.saveAuthnState,
      accessToken = options.accessToken,
      withCredentials = options.withCredentials === true, // default value is false
      storageUtil = sdk.options.storageUtil,
      storage = storageUtil!.storage,
      httpCache = sdk.storageManager.getHttpCache(sdk.options.cookies);

  if (options.cacheResponse) {
    var cacheContents = httpCache.getStorage();
    var cachedResponse = cacheContents[url as string];
    if (cachedResponse && Date.now()/1000 < cachedResponse.expiresAt) {
      return Promise.resolve(cachedResponse.response);
    }
  }

  var oktaUserAgentHeader = sdk._oktaUserAgent.getHttpHeader();
  var headers: HeadersInit = {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    ...oktaUserAgentHeader
  };
  Object.assign(headers, sdk.options.headers, options.headers);
  headers = removeNils(headers) as HeadersInit;

  if (accessToken && isString(accessToken)) {
    headers['Authorization'] = 'Bearer ' + accessToken;
  }

  var ajaxOptions: FetchOptions = {
    headers,
    data: args || undefined,
    withCredentials
  };

  var err, res;
  return sdk.options.httpRequestClient!(method!, url!, ajaxOptions)
    .then(function(resp) {
      res = resp.responseText;
      if (res && isString(res)) {
        res = JSON.parse(res);
        if (res && typeof res === 'object' && !res.headers) {
          if (Array.isArray(res)) {
            res.forEach(item => {
              item.headers = resp.headers;
            });
          } else {
            res.headers = resp.headers;
          }
        }
      }

      if (saveAuthnState) {
        if (!res.stateToken) {
          storage.delete(STATE_TOKEN_KEY_NAME);
        }
      }

      if (res && res.stateToken && res.expiresAt) {
        storage.set(STATE_TOKEN_KEY_NAME, res.stateToken, res.expiresAt, sdk.options.cookies!);
      }

      if (res && options.cacheResponse) {
        httpCache.updateStorage(url!, {
          expiresAt: Math.floor(Date.now()/1000) + DEFAULT_CACHE_DURATION,
          response: res
        });
      }

      return res;
    })
    .catch(function(resp) {
      err = formatError(sdk, resp);

      if (err.errorCode === 'E0000011') {
        storage.delete(STATE_TOKEN_KEY_NAME);
      }

      throw err;
    });
}

export function get(sdk: OktaAuthHttpInterface, url: string, options?: RequestOptions) {
  url = isAbsoluteUrl(url) ? url : sdk.getIssuerOrigin() + url;
  var getOptions = {
    url: url,
    method: 'GET'
  };
  Object.assign(getOptions, options);
  return httpRequest(sdk, getOptions);
}

export function post(sdk: OktaAuthHttpInterface, url: string, args?: RequestData, options?: RequestOptions) {
  url = isAbsoluteUrl(url) ? url : sdk.getIssuerOrigin() + url;
  var postOptions = {
    url: url,
    method: 'POST',
    args: args,
    saveAuthnState: true
  };
  Object.assign(postOptions, options);
  return httpRequest(sdk, postOptions);
}
