/*
 This file is part of GNU Taler
 (C) 2022 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 
 */
/**
 * Imports.
 */
import {
  RequestThrottler,
  TalerErrorCode,
  TalerError,
} from "@gnu-taler/taler-util";
import {
  Headers,
  HttpRequestLibrary,
  HttpRequestOptions,
  HttpResponse,
} from "@gnu-taler/taler-util/http";
/**
 * An implementation of the [[HttpRequestLibrary]] using the
 * browser's XMLHttpRequest.
 */
export class ServiceWorkerHttpLib implements HttpRequestLibrary {
  private throttle = new RequestThrottler();
  private throttlingEnabled = true;
  async fetch(
    requestUrl: string,
    options?: HttpRequestOptions,
  ): Promise {
    const requestMethod = options?.method ?? "GET";
    const requestBody = options?.body;
    const requestHeader = options?.headers;
    const requestTimeout = options?.timeout ?? { d_ms: 2 * 1000 };
    if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
      const parsedUrl = new URL(requestUrl);
      throw TalerError.fromDetail(
        TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
        {
          requestMethod,
          requestUrl,
          throttleStats: this.throttle.getThrottleStats(requestUrl),
        },
        `request to origin ${parsedUrl.origin} was throttled`,
      );
    }
    let myBody: BodyInit | undefined = undefined;
    if (requestBody != null) {
      if (typeof requestBody === "string") {
        myBody = requestBody;
      } else if (requestBody instanceof ArrayBuffer) {
        myBody = requestBody;
      } else if (ArrayBuffer.isView(requestBody)) {
        myBody = requestBody;
      } else if (typeof requestBody === "object") {
        myBody = JSON.stringify(requestBody);
      } else {
        throw Error("unsupported request body type");
      }
    }
    const controller = new AbortController();
    let timeoutId: any | undefined;
    if (requestTimeout.d_ms !== "forever") {
      timeoutId = setTimeout(() => {
        controller.abort(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT);
      }, requestTimeout.d_ms);
    }
    try {
      const response = await fetch(requestUrl, {
        headers: requestHeader,
        body: myBody,
        method: requestMethod,
        signal: controller.signal,
      });
      if (timeoutId) {
        clearTimeout(timeoutId);
      }
      const headerMap = new Headers();
      response.headers.forEach((value, key) => {
        headerMap.set(key, value);
      });
      return {
        headers: headerMap,
        status: response.status,
        requestMethod,
        requestUrl,
        json: makeJsonHandler(response, requestUrl, requestMethod),
        text: makeTextHandler(response, requestUrl, requestMethod),
        bytes: async () => (await response.blob()).arrayBuffer(),
      };
    } catch (e) {
      if (controller.signal) {
        throw TalerError.fromDetail(
          controller.signal.reason,
          {},
          `request to ${requestUrl} timed out`,
        );
      }
      throw e;
    }
  }
  get(url: string, opt?: HttpRequestOptions): Promise {
    return this.fetch(url, {
      method: "GET",
      ...opt,
    });
  }
  postJson(
    url: string,
    body: any,
    opt?: HttpRequestOptions,
  ): Promise {
    return this.fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
      ...opt,
    });
  }
  stop(): void {
    // Nothing to do
  }
}
function makeTextHandler(
  response: Response,
  requestUrl: string,
  requestMethod: string,
) {
  return async function getJsonFromResponse(): Promise {
    let respText;
    try {
      respText = await response.text();
    } catch (e) {
      throw TalerError.fromDetail(
        TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
        {
          requestUrl,
          requestMethod,
          httpStatusCode: response.status,
        },
        "Invalid JSON from HTTP response",
      );
    }
    return respText;
  };
}
function makeJsonHandler(
  response: Response,
  requestUrl: string,
  requestMethod: string,
) {
  return async function getJsonFromResponse(): Promise {
    let responseJson;
    try {
      responseJson = await response.json();
    } catch (e) {
      throw TalerError.fromDetail(
        TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
        {
          requestUrl,
          requestMethod,
          httpStatusCode: response.status,
        },
        "Invalid JSON from HTTP response",
      );
    }
    if (responseJson === null || typeof responseJson !== "object") {
      throw TalerError.fromDetail(
        TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
        {
          requestUrl,
          requestMethod,
          httpStatusCode: response.status,
        },
        "Invalid JSON from HTTP response",
      );
    }
    return responseJson;
  };
}