import { AuthApi } from "./AuthApi";

interface RequestParams {
  uri: string;
  method: "GET" | "POST" | "DELETE" | "PATCH" | "PUT";
  allowFail?: boolean;
  jsonData?: any;
  formData?: FormData;
  upProgress?: (progress: number) => void;
  downProgress?: (e: { progress: number; total: number }) => void;
}

interface APIResponse {
  data: any;
  status: number;
}

export class ApiError extends Error {
  constructor(message: string, public code: number, public data: any) {
    super(`HTTP status: ${code}\nMessage: ${message}\nData=${data}`);
  }
}

export class APIClient {
  /**
   * Get backend URL
   */
  static backendURL(): string {
    const URL = import.meta.env.VITE_APP_BACKEND ?? "";
    if (URL.length === 0) throw new Error("Backend URL undefined!");
    return URL;
  }

  /**
   * Get the full URL at which the backend can be contacted
   */
  static ActualBackendURL(): string {
    const backendURL = this.backendURL();
    if (backendURL.startsWith("/")) return `${location.origin}${backendURL}`;
    else return backendURL;
  }

  /**
   * Check out whether the backend is accessed through
   * HTTPS or not
   */
  static IsBackendSecure(): boolean {
    return this.ActualBackendURL().startsWith("https");
  }

  /**
   * Perform a request on the backend
   */
  static async exec(args: RequestParams): Promise<APIResponse> {
    let body: string | undefined | FormData = undefined;
    const headers: any = {};

    // JSON request
    if (args.jsonData) {
      headers["Content-Type"] = "application/json";
      body = JSON.stringify(args.jsonData);
    }

    // Form data request
    else if (args.formData) {
      body = args.formData;
    }

    const url = this.backendURL() + args.uri;

    let data;
    let status: number;

    // Make the request with XMLHttpRequest
    if (args.upProgress) {
      const res: XMLHttpRequest = await new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.upload.addEventListener("progress", (e) => {
          args.upProgress!(e.loaded / e.total);
        });
        xhr.addEventListener("load", () => {
          resolve(xhr);
        });
        xhr.addEventListener("error", () => {
          reject(new Error("File upload failed"));
        });
        xhr.addEventListener("abort", () => {
          reject(new Error("File upload aborted"));
        });
        xhr.addEventListener("timeout", () => {
          reject(new Error("File upload timeout"));
        });
        xhr.open(args.method, url, true);
        xhr.withCredentials = true;
        for (const key in headers) {
          if (Object.prototype.hasOwnProperty.call(headers, key))
            xhr.setRequestHeader(key, headers[key]);
        }
        xhr.send(body);
      });

      status = res.status;
      if (res.responseType === "json") data = JSON.parse(res.responseText);
      else data = res.response;
    }

    // Make the request with fetch
    else {
      const res = await fetch(url, {
        method: args.method,
        body: body,
        headers: headers,
        credentials: "include",
      });

      // Process response
      // JSON response
      if (res.headers.get("content-type") === "application/json")
        data = await res.json();
      // Text / XML response
      else if (
        ["application/xml", "text/plain"].includes(
          res.headers.get("content-type") ?? ""
        )
      )
        data = await res.text();
      // Binary file, tracking download progress
      else if (res.body !== null && args.downProgress) {
        // Track download progress
        const contentEncoding = res.headers.get("content-encoding");
        const contentLength = contentEncoding
          ? null
          : res.headers.get("content-length");

        const total = parseInt(contentLength ?? "0", 10);
        let loaded = 0;

        const resInt = new Response(
          new ReadableStream({
            start(controller) {
              const reader = res.body!.getReader();

              const read = async () => {
                try {
                  const ret = await reader.read();
                  if (ret.done) {
                    controller.close();
                    return;
                  }
                  loaded += ret.value.byteLength;
                  args.downProgress!({ progress: loaded, total });
                  controller.enqueue(ret.value);
                  read();
                } catch (e) {
                  console.error(e);
                  controller.error(e);
                }
              };

              read();
            },
          })
        );

        data = await resInt.blob();
      }

      // Do not track progress (binary file)
      else data = await res.blob();

      status = res.status;
    }

    // Handle expired tokens
    if (status === 412) {
      AuthApi.UnsetAuthenticated();
      window.location.href = "/";
    }

    if (!args.allowFail && (status < 200 || status > 299))
      throw new ApiError("Request failed!", status, data);

    return {
      data: data,
      status: status,
    };
  }
}
