import LRU from "lru-cache";
import { UpListing, UpObject } from ".";
import type {
  Address,
  AttributeListingResult,
  EntityListing,
  IJob,
  IValue,
  ListingResult,
  PutInput,
  PutResult,
  StoreInfo,
  VaultInfo,
} from "./types";

export class UpEndApi {
  private instanceUrl = "";

  private queryOnceLRU = new LRU<string, UpListing>({ max: 128 });
  private inFlightRequests: { [key: string]: Promise<UpListing> | null } = {};

  constructor(instanceUrl = "") {
    this.setInstanceUrl(instanceUrl);
  }

  public setInstanceUrl(apiUrl: string) {
    this.instanceUrl = apiUrl.replace(/\/+$/g, "");
  }

  public get apiUrl() {
    return this.instanceUrl + "/api";
  }

  public async fetchEntity(address: string): Promise<UpObject> {
    const entityFetch = await fetch(`${this.apiUrl}/obj/${address}`);
    const entityResult = (await entityFetch.json()) as EntityListing;
    const entityListing = new UpListing(entityResult.entries);
    return entityListing.getObject(address);
  }

  public async fetchEntry(address: string) {
    const response = await fetch(`${this.apiUrl}/raw/${address}`);
    const data = await response.json();
    const listing = new UpListing({ address: data });
    return listing.entries[0];
  }

  public async query(query: string): Promise<UpListing> {
    const cacheResult = this.queryOnceLRU.get(query);
    if (!cacheResult) {
      if (!this.inFlightRequests[query]) {
        console.debug(`Querying: ${query}`);
        this.inFlightRequests[query] = new Promise((resolve, reject) => {
          fetch(`${this.apiUrl}/query`, {
            method: "POST",
            body: query,
            keepalive: true,
          })
            .then(async (response) => {
              resolve(new UpListing(await response.json()));
              this.inFlightRequests[query] = null;
            })
            .catch((err) => reject(err));
        });
      } else {
        console.debug(`Chaining request for ${query}...`);
      }
      return await (this.inFlightRequests[query] as Promise<UpListing>); // TODO?
    } else {
      console.debug(`Returning cached: ${query}`);
      return cacheResult;
    }
  }

  public async putEntry(input: PutInput): Promise<PutResult> {
    const response = await fetch(`${this.apiUrl}/obj`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(input),
    });

    return await response.json();
  }

  public async putEntityAttribute(
    entity: Address,
    attribute: string,
    value: IValue
  ): Promise<Address> {
    const response = await fetch(`${this.apiUrl}/obj/${entity}/${attribute}`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(value),
    });

    return await response.json();
  }

  public async putBlob(fileOrUrl: File | URL): Promise<PutResult> {
    const formData = new FormData();
    if (fileOrUrl instanceof File) {
      formData.append(fileOrUrl.name, fileOrUrl);
    } else {
      formData.append("@url", fileOrUrl.toString());
    }

    const response = await fetch(`${this.apiUrl}/blob`, {
      method: "PUT",
      body: formData,
    });

    if (!response.ok) {
      throw Error(await response.text());
    }

    return await response.json();
  }

  public async deleteEntry(address: Address): Promise<void> {
    await fetch(`${this.apiUrl}/obj/${address}`, { method: "DELETE" });
  }

  public async getRaw(address: Address, preview = false) {
    return await fetch(
      `${this.apiUrl}/${preview ? "thumb" : "raw"}/${address}`
    );
  }

  public async refreshVault() {
    return await fetch(`${this.apiUrl}/refresh`, { method: "POST" });
  }

  public async nativeOpen(address: Address) {
    return fetch(`${this.apiUrl}/raw/${address}?native=1`);
  }

  public async fetchRoots(): Promise<ListingResult> {
    const response = await fetch(`${this.apiUrl}/hier_roots`);
    return await response.json();
  }

  public async fetchJobs(): Promise<IJob[]> {
    const response = await fetch(`${this.apiUrl}/jobs`);
    return await response.json();
  }

  public async fetchAllAttributes(): Promise<AttributeListingResult> {
    const response = await fetch(`${this.apiUrl}/all/attributes`);
    return await response.json();
  }

  public async fetchInfo(): Promise<VaultInfo> {
    const response = await fetch(`${this.apiUrl}/info`);
    return await response.json();
  }

  public async fetchStoreInfo(): Promise<{ [key: string]: StoreInfo }> {
    const response = await fetch(`${this.apiUrl}/store`);
    return await response.json();
  }

  public async getAddress(
    input: { attribute: string } | { url: string } | { urlContent: string }
  ): Promise<string> {
    let response;
    if ("attribute" in input) {
      response = await fetch(
        `${this.apiUrl}/address?attribute=${input.attribute}`
      );
    } else if ("url" in input) {
      response = await fetch(`${this.apiUrl}/address?url=${input.url}`);
    } else if ("urlContent" in input) {
      response = await fetch(
        `${this.apiUrl}/address?url_content=${input.urlContent}`
      );
    } else {
      throw new Error("Input cannot be empty.");
    }
    return await response.json();
  }
}
