/*
 This file is part of GNU Taler
 (C) 2021-2023 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 
 */
import { HttpStatusCode } from "@gnu-taler/taler-util";
import { base64encode } from "./base64.js";
export enum ErrorType {
  CLIENT,
  SERVER,
  UNREADABLE,
  TIMEOUT,
  UNEXPECTED,
}
/**
 *
 * @param baseUrl URL where the service is located
 * @param endpoint endpoint of the service to be called
 * @param options auth, method and params
 * @returns
 */
export async function defaultRequestHandler(
  baseUrl: string,
  endpoint: string,
  options: RequestOptions = {},
): Promise> {
  const requestHeaders: Record = {};
  if (options.token) {
    requestHeaders.Authorization = `Bearer ${options.token}`;
  } else if (options.basicAuth) {
    requestHeaders.Authorization = `Basic ${base64encode(
      `${options.basicAuth.username}:${options.basicAuth.password}`,
    )}`;
  }
  requestHeaders["Content-Type"] =
    options.contentType === "json" ? "application/json" : "text/plain";
  const requestMethod = options?.method ?? "GET";
  const requestBody = options?.data;
  const requestTimeout = options?.timeout ?? 5 * 1000;
  const requestParams = options.params ?? {};
  const requestPreventCache = options.preventCache ?? false;
  const requestPreventCors = options.preventCors ?? false;
  const _url = new URL(`${baseUrl}${endpoint}`);
  Object.entries(requestParams).forEach(([key, value]) => {
    _url.searchParams.set(key, String(value));
  });
  let payload: BodyInit | undefined = undefined;
  if (requestBody != null) {
    if (typeof requestBody === "string") {
      payload = requestBody;
    } else if (requestBody instanceof ArrayBuffer) {
      payload = requestBody;
    } else if (ArrayBuffer.isView(requestBody)) {
      payload = requestBody;
    } else if (typeof requestBody === "object") {
      payload = JSON.stringify(requestBody);
    } else {
      throw Error("unsupported request body type");
    }
  }
  const controller = new AbortController();
  const timeoutId = setTimeout(() => {
    controller.abort("HTTP_REQUEST_TIMEOUT");
  }, requestTimeout);
  let response;
  try {
    response = await fetch(_url.href, {
      headers: requestHeaders,
      method: requestMethod,
      credentials: "omit",
      mode: requestPreventCors ? "no-cors" : "cors",
      cache: requestPreventCache ? "no-cache" : "default",
      body: payload,
      signal: controller.signal,
    });
  } catch (ex) {
    const info: RequestInfo = {
      payload,
      url: _url.href,
      hasToken: !!options.token,
      status: 0,
      options,
    };
    const error: HttpRequestTimeoutError = {
      info,
      type: ErrorType.TIMEOUT,
      message: "Request timeout",
    };
    throw new RequestError(error);
  }
  if (timeoutId) {
    clearTimeout(timeoutId);
  }
  const headerMap = new Headers();
  response.headers.forEach((value, key) => {
    headerMap.set(key, value);
  });
  if (response.ok) {
    const result = await buildRequestOk(
      response,
      _url.href,
      payload,
      !!options.token,
      options,
    );
    return result;
  } else {
    const dataTxt = await response.text();
    const error = buildRequestFailed(
      _url.href,
      dataTxt,
      response.status,
      payload,
      options,
    );
    throw new RequestError(error);
  }
}
export type HttpResponse =
  | HttpResponseOk
  | HttpResponseLoading
  | HttpError;
export type HttpResponsePaginated =
  | HttpResponseOkPaginated
  | HttpResponseLoading
  | HttpError;
export interface RequestInfo {
  url: string;
  hasToken: boolean;
  payload: any;
  status: number;
  options: RequestOptions;
}
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 =
  | HttpRequestTimeoutError
  | HttpResponseClientError
  | HttpResponseServerError
  | HttpResponseUnreadableError
  | HttpResponseUnexpectedError;
export interface HttpResponseServerError {
  ok?: false;
  loading?: false;
  type: ErrorType.SERVER;
  payload: ErrorDetail;
  status: HttpStatusCode;
  message: string;
  info: RequestInfo;
}
interface HttpRequestTimeoutError {
  ok?: false;
  loading?: false;
  type: ErrorType.TIMEOUT;
  info: RequestInfo;
  message: string;
}
interface HttpResponseClientError {
  ok?: false;
  loading?: false;
  type: ErrorType.CLIENT;
  info: RequestInfo;
  status: HttpStatusCode;
  payload: ErrorDetail;
  message: string;
}
interface HttpResponseUnexpectedError {
  ok?: false;
  loading: false;
  type: ErrorType.UNEXPECTED;
  info: RequestInfo;
  status?: HttpStatusCode;
  exception: unknown;
  message: string;
}
interface HttpResponseUnreadableError {
  ok?: false;
  loading: false;
  type: ErrorType.UNREADABLE;
  info: RequestInfo;
  status: HttpStatusCode;
  exception: unknown;
  body: string;
  message: string;
}
export class RequestError extends Error {
  /**
   * @deprecated use cause
   */
  info: HttpError;
  cause: HttpError;
  constructor(d: HttpError) {
    super(d.message);
    this.info = d;
    this.cause = d;
  }
}
type Methods = "GET" | "POST" | "PATCH" | "DELETE" | "PUT";
export interface RequestOptions {
  method?: Methods;
  token?: string;
  basicAuth?: {
    username: string;
    password: string;
  };
  preventCache?: boolean;
  preventCors?: boolean;
  data?: any;
  params?: unknown;
  timeout?: number;
  contentType?: "text" | "json";
}
async function buildRequestOk(
  response: Response,
  url: string,
  payload: any,
  hasToken: boolean,
  options: RequestOptions,
): Promise> {
  const dataTxt = await response.text();
  const data = dataTxt ? JSON.parse(dataTxt) : undefined;
  return {
    ok: true,
    data,
    info: {
      payload,
      url,
      hasToken,
      options,
      status: response.status,
    },
  };
}
export function buildRequestFailed(
  url: string,
  dataTxt: string,
  status: number,
  payload: any,
  maybeOptions?: RequestOptions,
):
  | HttpResponseClientError
  | HttpResponseServerError
  | HttpResponseUnreadableError
  | HttpResponseUnexpectedError {
  const options = maybeOptions ?? {};
  const info: RequestInfo = {
    payload,
    url,
    hasToken: !!options.token,
    options,
    status: status || 0,
  };
  // const dataTxt = await response.text();
  try {
    const data = dataTxt ? JSON.parse(dataTxt) : undefined;
    const errorCode = !data || !data.code ? "" : `(code: ${data.code})`;
    const errorHint =
      !data || !data.hint ? "Not hint." : `${data.hint} ${errorCode}`;
    if (status && status >= 400 && status < 500) {
      const message =
        data === undefined
          ? `Client error (${status}) without data.`
          : errorHint;
      const error: HttpResponseClientError = {
        type: ErrorType.CLIENT,
        status,
        info,
        message,
        payload: data,
      };
      return error;
    }
    if (status && status >= 500 && status < 600) {
      const message =
        data === undefined
          ? `Server error (${status}) without data.`
          : errorHint;
      const error: HttpResponseServerError = {
        type: ErrorType.SERVER,
        status,
        info,
        message,
        payload: data,
      };
      return error;
    }
    return {
      info,
      loading: false,
      type: ErrorType.UNEXPECTED,
      status,
      exception: undefined,
      message: `http status code not handled: ${status}`,
    };
  } catch (ex) {
    const error: HttpResponseUnreadableError = {
      info,
      loading: false,
      status,
      type: ErrorType.UNREADABLE,
      exception: ex,
      body: dataTxt,
      message: "Could not parse body as json",
    };
    return error;
  }
}