/*
 This file is part of GNU Taler
 (C) 2021 Taler Systems S.A.
 GNU Taler 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; either version 3, or (at your option) any later version.
 GNU Taler 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
 GNU Taler; see the file COPYING.  If not, see 
 */
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { mutate, cache } from 'swr';
import axios, { AxiosError, AxiosResponse } from 'axios'
import { MerchantBackend } from '../declaration';
import { useBackendContext } from '../context/backend';
import { useEffect, useState } from 'preact/hooks';
import { DEFAULT_REQUEST_TIMEOUT } from '../utils/constants';
export function mutateAll(re: RegExp, value?: unknown): Array> {
  return cache.keys().filter(key => {
    return re.test(key)
  }).map(key => {
    return mutate(key, value)
  })
}
export type HttpResponse = HttpResponseOk | HttpResponseLoading | HttpError;
export type HttpResponsePaginated = HttpResponseOkPaginated | HttpResponseLoading | HttpError;
export interface RequestInfo {
  url: string;
  hasToken: boolean;
  params: unknown;
  data: unknown;
}
interface HttpResponseLoading {
  ok?: false;
  loading: true;
  clientError?: false;
  serverError?: false;
  data?: T;
}
export interface HttpResponseOk {
  ok: true;
  loading?: false;
  clientError?: false;
  serverError?: false;
  data: T;
  info?: RequestInfo;
}
export type HttpResponseOkPaginated = HttpResponseOk & WithPagination
export interface WithPagination {
  loadMore: () => void;
  loadMorePrev: () => void;
  isReachingEnd?: boolean;
  isReachingStart?: boolean;
}
export type HttpError = HttpResponseClientError | HttpResponseServerError | HttpResponseUnexpectedError;
export interface SwrError {
  info: unknown,
  status: number,
  message: string,
}
export interface HttpResponseServerError {
  ok?: false;
  loading?: false;
  clientError?: false;
  serverError: true;
  error?: MerchantBackend.ErrorDetail;
  status: number;
  message: string;
  info?: RequestInfo;
}
interface HttpResponseClientError {
  ok?: false;
  loading?: false;
  clientError: true;
  serverError?: false;
  info?: RequestInfo;
  isUnauthorized: boolean;
  isNotfound: boolean;
  status: number;
  error?: MerchantBackend.ErrorDetail;
  message: string;
}
interface HttpResponseUnexpectedError {
  ok?: false;
  loading?: false;
  clientError?: false;
  serverError?: false;
  info?: RequestInfo;
  status?: number;
  error: unknown;
  message: string;
}
type Methods = 'get' | 'post' | 'patch' | 'delete' | 'put';
interface RequestOptions {
  method?: Methods;
  token?: string;
  data?: unknown;
  params?: unknown;
}
function buildRequestOk(res: AxiosResponse, url: string, hasToken: boolean): HttpResponseOk {
  return {
    ok: true, data: res.data, info: {
      params: res.config.params,
      data: res.config.data,
      url,
      hasToken,
    }
  }
}
// function buildResponse(data?: T, error?: MerchantBackend.ErrorDetail, isValidating?: boolean): HttpResponse {
//   if (isValidating) return {loading: true}
//   if (error) return buildRequestFailed()
// }
function buildRequestFailed(ex: AxiosError, url: string, hasToken: boolean): HttpResponseClientError | HttpResponseServerError | HttpResponseUnexpectedError {
  const status = ex.response?.status
  const info: RequestInfo = {
    data: ex.request?.data,
    params: ex.request?.params,
    url,
    hasToken,
  };
  if (status && status >= 400 && status < 500) {
    const error: HttpResponseClientError = {
      clientError: true,
      isNotfound: status === 404,
      isUnauthorized: status === 401,
      status,
      info,
      message: ex.response?.data?.hint || ex.message,
      error: ex.response?.data
    }
    return error
  }
  if (status && status >= 500 && status < 600) {
    const error: HttpResponseServerError = {
      serverError: true,
      status,
      info,
      message: `${ex.response?.data?.hint} (code ${ex.response?.data?.code})` || ex.message,
      error: ex.response?.data
    }
    return error;
  }
  const error: HttpResponseUnexpectedError = {
    info,
    status,
    error: ex,
    message: ex.message
  }
  return error
}
const CancelToken = axios.CancelToken;
let source = CancelToken.source();
export function cancelPendingRequest() {
  source.cancel('canceled by the user')
  source = CancelToken.source()
}
let removeAxiosCancelToken = false
/**
 * Jest mocking seems to break when using the cancelToken property.
 * Using this workaround when testing while finding the correct solution
 */
export function setAxiosRequestAsTestingEnvironment() {
  removeAxiosCancelToken = true
}
export async function request(url: string, options: RequestOptions = {}): Promise> {
  const headers = options.token ? { Authorization: `Bearer ${options.token}` } : undefined
  try {
    const res = await axios({
      url,
      responseType: 'json',
      headers,
      cancelToken: !removeAxiosCancelToken ? source.token : undefined,
      method: options.method || 'get',
      data: options.data,
      params: options.params,
      timeout: DEFAULT_REQUEST_TIMEOUT * 1000,
    })
    return buildRequestOk(res, url, !!options.token)
  } catch (e) {
    if (axios.isAxiosError(e)) {
      throw buildRequestFailed(e, url, !!options.token)
    }
    throw e
  }
}
export function fetcher(url: string, token: string, backend: string): Promise> {
  return request(`${backend}${url}`, { token })
}
export function useBackendInstancesTestForAdmin(): HttpResponse {
  const { url, token } = useBackendContext()
  type Type = MerchantBackend.Instances.InstancesResponse;
  const [result, setResult] = useState>({ loading: true })
  useEffect(() => {
    request(`${url}/management/instances`, { token })
      .then(data => setResult(data))
      .catch(error => setResult(error))
  }, [url, token])
  return result
}
export function useBackendConfig(): HttpResponse {
  const { url, token } = useBackendContext()
  type Type = MerchantBackend.VersionResponse;
  const [result, setResult] = useState>({ loading: true })
  useEffect(() => {
    request(`${url}/config`, { token })
      .then(data => setResult(data))
      .catch(error => setResult(error))
  }, [url, token])
  return result
}