/*
 This file is part of GNU Taler
 (C) 2021-2025 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 <http://www.gnu.org/licenses/>
 */

import { HttpStatusCode } from "@gnu-taler/taler-util";
import { base64encode } from "./base64.js";

/**
 * @deprecated do not use it, it will be removed
 */
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
 * @deprecated do not use it, it will be removed
 * @returns
 */
export async function defaultRequestHandler<T>(
  baseUrl: string,
  endpoint: string,
  options: RequestOptions = {},
): Promise<HttpResponseOk<T>> {
  const requestHeaders: Record<string, string> = {};
  if (options.token) {
    requestHeaders.Authorization = `Bearer secret-token:${options.token}`;
  } else if (options.basicAuth) {
    requestHeaders.Authorization = `Basic ${base64encode(
      `${options.basicAuth.username}:${options.basicAuth.password}`,
    )}`;
  }

  requestHeaders["Content-Type"] =
    !options.contentType || options.contentType === "json"
      ? "application/json"
      : "text/plain";

  if (options.talerAmlOfficerSignature) {
    requestHeaders["Taler-AML-Officer-Signature"] =
      options.talerAmlOfficerSignature;
  }

  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 validURL = validateURL(baseUrl, endpoint);

  if (!validURL) {
    const error: HttpResponseUnexpectedError = {
      info: {
        url: `${baseUrl}${endpoint}`,
        payload: {},
        hasToken: !!options.token,
        status: 0,
        options,
      },
      type: ErrorType.UNEXPECTED,
      exception: undefined,
      loading: false,
      message: `invalid URL: "${baseUrl}${endpoint}"`,
    };
    throw new RequestError(error);
  }

  Object.entries(requestParams).forEach(([key, value]) => {
    validURL.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 {
      const error: HttpResponseUnexpectedError = {
        info: {
          url: validURL.href,
          payload: {},
          hasToken: !!options.token,
          status: 0,
          options,
        },
        type: ErrorType.UNEXPECTED,
        exception: undefined,
        loading: false,
        message: `unsupported request body type: "${typeof requestBody}"`,
      };
      throw new RequestError(error);
    }
  }

  const controller = new AbortController();
  const timeoutId = setTimeout(() => {
    controller.abort("HTTP_REQUEST_TIMEOUT");
  }, requestTimeout);

  let response;
  try {
    response = await fetch(validURL.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: validURL.href,
      hasToken: !!options.token,
      status: 0,
      options,
    };

    if (ex instanceof Error) {
      if (ex.message === "HTTP_REQUEST_TIMEOUT") {
        const error: HttpRequestTimeoutError = {
          info,
          type: ErrorType.TIMEOUT,
          message: "request timeout",
        };
        throw new RequestError(error);
      }
    }

    const error: HttpResponseUnexpectedError = {
      info,
      type: ErrorType.UNEXPECTED,
      exception: ex,
      loading: false,
      message: ex instanceof Error ? ex.message : "",
    };
    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<T>(
      response,
      validURL.href,
      payload,
      !!options.token,
      options,
    );
    return result;
  } else {
    const dataTxt = await response.text();
    const error = buildRequestFailed(
      validURL.href,
      dataTxt,
      response.status,
      payload,
      options,
    );
    throw new RequestError(error);
  }
}

/**
 * @deprecated do not use it, it will be removed
 */
export type HttpResponse<T, ErrorDetail> =
  | HttpResponseOk<T>
  | HttpResponseLoading<T>
  | HttpError<ErrorDetail>;

/**
 * @deprecated do not use it, it will be removed
 */
export type HttpResponsePaginated<T, ErrorDetail> =
  | HttpResponseOkPaginated<T>
  | HttpResponseLoading<T>
  | HttpError<ErrorDetail>;

/**
 * @deprecated do not use it, it will be removed
 */
export interface RequestInfo {
  url: string;
  hasToken: boolean;
  payload: any;
  status: number;
  options: RequestOptions;
}

interface HttpResponseLoading<T> {
  ok?: false;
  loading: true;
  clientError?: false;
  serverError?: false;

  data?: T;
}
/**
 * @deprecated do not use it, it will be removed
 */
export interface HttpResponseOk<T> {
  ok: true;
  loading?: false;
  clientError?: false;
  serverError?: false;

  data: T;
  info?: RequestInfo;
}

/**
 * @deprecated do not use it, it will be removed
 */
export type HttpResponseOkPaginated<T> = HttpResponseOk<T> & WithPagination;

/**
 * @deprecated do not use it, it will be removed
 */
export interface WithPagination {
  loadMore: () => void;
  loadMorePrev: () => void;
  isReachingEnd?: boolean;
  isReachingStart?: boolean;
}

/**
 * @deprecated do not use it, it will be removed
 */
export type HttpError<ErrorDetail> =
  | HttpRequestTimeoutError
  | HttpResponseClientError<ErrorDetail>
  | HttpResponseServerError<ErrorDetail>
  | HttpResponseUnreadableError
  | HttpResponseUnexpectedError;

/**
 * @deprecated do not use it, it will be removed
 */
export interface HttpResponseServerError<ErrorDetail> {
  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<ErrorDetail> {
  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;
}
/**
 * @deprecated do not use it, it will be removed
 */
export class RequestError<ErrorDetail> extends Error {
  /**
   * @deprecated use cause
   */
  info: HttpError<ErrorDetail>;
  cause: HttpError<ErrorDetail>;
  constructor(d: HttpError<ErrorDetail>) {
    super(d.message);
    this.info = d;
    this.cause = d;
  }
}

type Methods = "GET" | "POST" | "PATCH" | "DELETE" | "PUT";

/**
 * @deprecated do not use it, it will be removed
 */
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";
  talerAmlOfficerSignature?: string;
}

/**
 * @deprecated do not use it, it will be removed
 */
async function buildRequestOk<T>(
  response: Response,
  url: string,
  payload: any,
  hasToken: boolean,
  options: RequestOptions,
): Promise<HttpResponseOk<T>> {
  const dataTxt = await response.text();
  const data = dataTxt ? JSON.parse(dataTxt) : undefined;
  return {
    ok: true,
    data,
    info: {
      payload,
      url,
      hasToken,
      options,
      status: response.status,
    },
  };
}

/**
 * @deprecated do not use it, it will be removed
 */
export function buildRequestFailed<ErrorDetail>(
  url: string,
  dataTxt: string,
  status: number,
  payload: any,
  maybeOptions?: RequestOptions,
):
  | HttpResponseClientError<ErrorDetail>
  | HttpResponseServerError<ErrorDetail>
  | 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<ErrorDetail> = {
        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<ErrorDetail> = {
        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;
  }
}

/**
 * @deprecated do not use it, it will be removed
 */
function validateURL(baseUrl: string, endpoint: string): URL | undefined {
  try {
    return new URL(`${baseUrl}${endpoint}`);
  } catch (ex) {
    return undefined;
  }
}
