/*
 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 <http://www.gnu.org/licenses/>
 */

/**
 * Imports.
 */
import {
  Duration,
  Logger,
  RequestThrottler,
  TalerError,
  TalerErrorCode,
} from "@gnu-taler/taler-util";

import {
  DEFAULT_REQUEST_TIMEOUT_MS,
  HeadersImpl,
  HttpLibArgs,
  HttpRequestLibrary,
  HttpRequestOptions,
  HttpResponse,
  encodeBody,
  getDefaultHeaders,
} from "@gnu-taler/taler-util/http";

const logger = new Logger("browserHttpLib");

/**
 * An implementation of the [[HttpRequestLibrary]] using the
 * browser's XMLHttpRequest.
 *
 * @deprecated use BrowserFetchHttpLib
 */
export class BrowserHttpLibDepreacted implements HttpRequestLibrary {
  private throttle = new RequestThrottler();
  private throttlingEnabled = true;
  private requireTls = false;

  constructor(args?: HttpLibArgs) {
    this.throttlingEnabled = args?.enableThrottling ?? true;
    this.requireTls = args?.requireTls ?? false;
  }

  async fetch(
    requestUrl: string,
    options?: HttpRequestOptions,
  ): Promise<HttpResponse> {
    const requestMethod = options?.method ?? "GET";
    const requestBody = options?.body;
    const requestHeader = options?.headers;
    const requestTimeout =
      options?.timeout ?? Duration.fromMilliseconds(DEFAULT_REQUEST_TIMEOUT_MS);

    const parsedUrl = new URL(requestUrl);
    if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
      throw TalerError.fromDetail(
        TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
        {
          requestMethod,
          requestUrl,
          throttleStats: this.throttle.getThrottleStats(requestUrl),
        },
        `request to origin ${parsedUrl.origin} was throttled`,
      );
    }
    if (this.requireTls && parsedUrl.protocol !== "https:") {
      throw TalerError.fromDetail(
        TalerErrorCode.WALLET_NETWORK_ERROR,
        {
          requestMethod: requestMethod,
          requestUrl: requestUrl,
        },
        `request to ${parsedUrl.origin} is not possible with protocol ${parsedUrl.protocol}`,
      );
    }

    const encodedBody: ArrayBuffer | undefined =
      requestMethod === "POST" ||
      requestMethod === "PUT" ||
      requestMethod === "PATCH"
        ? encodeBody(requestBody)
        : undefined;

    const myBody =
      !options?.compress || !encodedBody
        ? encodedBody
        : (await compress(options.compress, encodedBody));

    const requestHeadersMap = getDefaultHeaders(requestMethod);
    if (requestHeader) {
      Object.entries(requestHeader).forEach(([key, value]) => {
        if (value === undefined) return;
        requestHeadersMap[key] = value;
      });
    }

    if (options?.compress) {
      requestHeadersMap["Content-Encoding"] = options.compress
    }

    return new Promise<HttpResponse>((resolve, reject) => {
      const myRequest = new XMLHttpRequest();

      myRequest.onerror = (e) => {
        logger.error("http request error");
        reject(
          TalerError.fromDetail(
            TalerErrorCode.WALLET_NETWORK_ERROR,
            {
              requestUrl,
              requestMethod,
            },
            "Could not make request",
          ),
        );
      };

      myRequest.open(requestMethod, requestUrl);

      let timeoutId: any | undefined;
      if (requestTimeout.d_ms !== "forever") {
        timeoutId = setTimeout(() => {
          myRequest.abort();
          reject(
            TalerError.fromDetail(
              TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT,
              {
                requestUrl,
                requestMethod,
                timeoutMs:
                  requestTimeout.d_ms === "forever" ? 0 : requestTimeout.d_ms,
              },
              `request to ${requestUrl} timed out`,
            ),
          );
        }, requestTimeout.d_ms);
      }

      Object.keys(requestHeadersMap).forEach((headerName) => {
        myRequest.setRequestHeader(headerName, requestHeadersMap[headerName]);
      });

      myRequest.responseType = "arraybuffer";
      myRequest.send(myBody);

      myRequest.addEventListener("readystatechange", (e) => {
        if (myRequest.readyState === XMLHttpRequest.DONE) {
          if (myRequest.status === 0) {
            const exc = TalerError.fromDetail(
              TalerErrorCode.WALLET_NETWORK_ERROR,
              {
                requestUrl,
                requestMethod,
              },
              "HTTP request failed (status 0, maybe URI scheme was wrong?)",
            );
            reject(exc);
            return;
          }
          const makeText = async (): Promise<string> => {
            const td = new TextDecoder();
            return td.decode(myRequest.response);
          };
          let responseJson: unknown = undefined;
          const makeJson = async (): Promise<any> => {
            if (responseJson === undefined) {
              try {
                const td = new TextDecoder();
                const responseString = td.decode(myRequest.response);
                responseJson = JSON.parse(responseString);
              } catch (e) {
                throw TalerError.fromDetail(
                  TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
                  {
                    requestUrl,
                    requestMethod,
                    httpStatusCode: myRequest.status,
                  },
                  "Invalid JSON from HTTP response",
                );
              }
            }
            if (responseJson === null || typeof responseJson !== "object") {
              throw TalerError.fromDetail(
                TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
                {
                  requestUrl,
                  requestMethod,
                  httpStatusCode: myRequest.status,
                },
                "Invalid JSON from HTTP response",
              );
            }
            return responseJson;
          };

          const headers = myRequest.getAllResponseHeaders();
          const arr = headers.trim().split(/[\r\n]+/);

          // Create a map of header names to values
          const headerMap: HeadersImpl = new HeadersImpl();
          arr.forEach(function (line) {
            const parts = line.split(": ");
            const headerName = parts.shift();
            if (!headerName) {
              logger.warn("skipping invalid header");
              return;
            }
            const value = parts.join(": ");
            headerMap.set(headerName, value);
          });
          const resp: HttpResponse = {
            requestUrl: requestUrl,
            status: myRequest.status,
            headers: headerMap,
            requestMethod: requestMethod,
            json: makeJson,
            text: makeText,
            bytes: async () => myRequest.response,
          };
          resolve(resp);
        }
      });
    });
  }

  get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
    return this.fetch(url, {
      method: "GET",
      ...opt,
    });
  }

  postJson(
    url: string,
    body: any,
    opt?: HttpRequestOptions,
  ): Promise<HttpResponse> {
    return this.fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
      ...opt,
    });
  }

  stop(): void {
    // Nothing to do
  }
}

export async function compress(
  alg: "gzip" | "deflate" | undefined,
  buf: ArrayBuffer,
): Promise<ArrayBuffer> {
  if (!alg) return buf;
  const cs = new CompressionStream(alg);
  const writer = cs.writable.getWriter();
  writer.write(buf);
  writer.close();
  return new Response(cs.readable).arrayBuffer();
}