/*
 This file is part of GNU Taler
 (C) 2019 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 
 SPDX-License-Identifier: AGPL3.0-or-later
*/
/**
 * Imports.
 */
import * as http from "node:http";
import * as https from "node:https";
import { RequestOptions } from "node:http";
import { TalerError } from "./errors.js";
import { encodeBody, getDefaultHeaders, HttpLibArgs } from "./http-common.js";
import {
  DEFAULT_REQUEST_TIMEOUT_MS,
  Headers,
  HttpRequestLibrary,
  HttpRequestOptions,
  HttpResponse,
} from "./http.js";
import {
  Logger,
  RequestThrottler,
  TalerErrorCode,
  typedArrayConcat,
  URL,
} from "./index.js";
const logger = new Logger("http-impl.node.ts");
const textDecoder = new TextDecoder();
/**
 * Implementation of the HTTP request library interface for node.
 */
export class HttpLibImpl implements HttpRequestLibrary {
  private throttle = new RequestThrottler();
  private throttlingEnabled = true;
  private allowHttp = false;
  constructor(args?: HttpLibArgs) {
    this.throttlingEnabled = args?.enableThrottling ?? false;
    this.allowHttp = args?.allowHttp ?? false;
  }
  /**
   * Set whether requests should be throttled.
   */
  setThrottling(enabled: boolean): void {
    this.throttlingEnabled = enabled;
  }
  async fetch(url: string, opt?: HttpRequestOptions): Promise {
    const method = opt?.method?.toUpperCase() ?? "GET";
    logger.trace(`Requesting ${method} ${url}`);
    const parsedUrl = new URL(url);
    if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
      throw TalerError.fromDetail(
        TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
        {
          requestMethod: method,
          requestUrl: url,
          throttleStats: this.throttle.getThrottleStats(url),
        },
        `request to origin ${parsedUrl.origin} was throttled`,
      );
    }
    if (!this.allowHttp && parsedUrl.protocol !== "https:") {
      throw TalerError.fromDetail(
        TalerErrorCode.WALLET_NETWORK_ERROR,
        {
          requestMethod: method,
          requestUrl: url,
        },
        `request to ${parsedUrl.origin} is not possible with protocol ${parsedUrl.protocol}`,
      );
    }
    let timeoutMs: number | undefined;
    if (typeof opt?.timeout?.d_ms === "number") {
      timeoutMs = opt.timeout.d_ms;
    } else {
      timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
    }
    const requestHeadersMap = { ...getDefaultHeaders(method), ...opt?.headers };
    let reqBody: ArrayBuffer | undefined;
    if (opt?.method == "POST") {
      reqBody = encodeBody(opt.body);
    }
    let path = parsedUrl.pathname;
    if (parsedUrl.search != null) {
      path += parsedUrl.search;
    }
    let protocol: string;
    if (parsedUrl.protocol === "https:") {
      protocol = "https:";
    } else if (parsedUrl.protocol === "http:") {
      protocol = "http:";
    } else {
      throw Error(`unsupported protocol (${parsedUrl.protocol})`);
    }
    const options: RequestOptions = {
      protocol,
      port: parsedUrl.port,
      host: parsedUrl.hostname,
      method: method,
      path,
      headers: requestHeadersMap,
    };
    const chunks: Uint8Array[] = [];
    return new Promise((resolve, reject) => {
      const handler = (res: http.IncomingMessage) => {
        res.on("data", (d) => {
          chunks.push(d);
        });
        res.on("end", () => {
          const headers: Headers = new Headers();
          for (const [k, v] of Object.entries(res.headers)) {
            if (!v) {
              continue;
            }
            if (typeof v === "string") {
              headers.set(k, v);
            } else {
              headers.set(k, v.join(", "));
            }
          }
          const data = typedArrayConcat(chunks);
          const resp: HttpResponse = {
            requestMethod: method,
            requestUrl: parsedUrl.href,
            status: res.statusCode || 0,
            headers,
            async bytes() {
              return data;
            },
            json() {
              const text = textDecoder.decode(data);
              return JSON.parse(text);
            },
            async text() {
              const text = textDecoder.decode(data);
              return text;
            },
          };
          resolve(resp);
        });
        res.on("error", (e) => {
          reject(e);
        });
      };
      let req: http.ClientRequest;
      if (options.protocol === "http:") {
        req = http.request(options, handler);
      } else if (options.protocol === "https:") {
        req = https.request(options, handler);
      } else {
        throw new Error(`unsupported protocol ${options.protocol}`);
      }
      req.on("error", (e: Error) => {
        reject(e);
      });
      if (reqBody) {
        req.write(new Uint8Array(reqBody));
      }
      req.end();
    });
  }
  async get(url: string, opt?: HttpRequestOptions): Promise {
    return this.fetch(url, {
      method: "GET",
      ...opt,
    });
  }
  async postJson(
    url: string,
    body: any,
    opt?: HttpRequestOptions,
  ): Promise {
    return this.fetch(url, {
      method: "POST",
      body,
      ...opt,
    });
  }
}