import { validateImportedEntity } from "$lib/extension/transporting/validators";
import { exportEntityToObject } from "$lib/extension/transporting/exporters";
import StorageEntity from "$lib/extension/base/StorageEntity";
import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from "lz-string";
import type { ImportableElement } from "$lib/extension/transporting/importables";

/**
 * Status of the last import.
 *
 * - `NULL` - no import was done yet or was unsuccessful.
 * - `"unknown"` — imported object was created before v0.5, when extension started to be built for multiple sites.
 * - `"same"` — imported object is marked as generated by the same type of extension.
 * - `"different"` — imported object is marked as generated by some other type of extension.
 */
export type SameSiteStatus = null | "unknown" | "same" | "different";

export default class EntitiesTransporter<EntityType> {
  readonly #targetEntityConstructor: new (...any: any[]) => EntityType;

  #lastSameSiteStatus: SameSiteStatus = null;

  /**
   * Read the status of the last successful import. This flag could be used to determine if it was for the same site as
   * the current extension or when it's generated before site identity was passed to the importable object.
   *
   * @see {SameSiteStatus} For the list of possible statuses.
   */
  get lastImportSameSiteStatus() {
    return this.#lastSameSiteStatus;
  }

  /**
   * Name of the entity, exported directly from the constructor.
   * @private
   */
  get #entityName() {
    // How the hell should I even do this?
    const entityName = ((this.#targetEntityConstructor as any) as typeof StorageEntity)._entityName;

    if (entityName === "entity") {
      throw new Error("Generic entity name encountered!");
    }

    return entityName;
  }

  /**
   * @param entityConstructor Class which should be used for import or export.
   */
  constructor(entityConstructor: new (...any: any[]) => EntityType) {
    if (!(entityConstructor.prototype instanceof StorageEntity)) {
      throw new TypeError('Invalid class provided as the target for importing!');
    }

    this.#targetEntityConstructor = entityConstructor;
  }

  isCorrectEntity(entityObject: unknown): entityObject is EntityType {
    return entityObject instanceof this.#targetEntityConstructor;
  }

  importFromObject(importedObject: Record<string, any>): EntityType {
    this.#lastSameSiteStatus = null;

    // TODO: There should be an auto-upgrader somewhere before the validation. So if even the older version of schema
    //       was used, we still will will be able to pass the validation. For now we only have non-breaking changes.
    validateImportedEntity(
      this.#entityName,
      importedObject,
    );

    this.#lastSameSiteStatus = EntitiesTransporter.checkIsSameSiteImportedObject(importedObject);

    return new this.#targetEntityConstructor(
      importedObject.id,
      importedObject
    );
  }

  importFromJSON(jsonString: string): EntityType {
    const importedObject = this.#tryParsingAsJSON(jsonString);

    if (!importedObject) {
      this.#lastSameSiteStatus = null;
      throw new Error('Invalid JSON!');
    }

    return this.importFromObject(importedObject);
  }

  importFromCompressedJSON(compressedJsonString: string): EntityType {
    return this.importFromJSON(
      decompressFromEncodedURIComponent(compressedJsonString)
    )
  }

  exportToObject(entityObject: EntityType) {
    if (!this.isCorrectEntity(entityObject)) {
      throw new TypeError('Transporter should be connected to the same entity to export!');
    }

    if (!(entityObject instanceof StorageEntity)) {
      throw new TypeError('Only storage entities could be exported!');
    }

    return exportEntityToObject(
      this.#entityName,
      entityObject
    );
  }

  exportToJSON(entityObject: EntityType): string {
    return JSON.stringify(
      this.exportToObject(entityObject),
      null,
      2
    );
  }

  exportToCompressedJSON(entityObject: EntityType): string {
    return compressToEncodedURIComponent(this.exportToJSON(entityObject));
  }

  #tryParsingAsJSON(jsonString: string): Record<string, any> | null {
    let jsonObject: Record<string, any> | null = null;

    try {
      jsonObject = JSON.parse(jsonString);
    } catch (e) {

    }

    if (typeof jsonObject !== "object") {
      throw new TypeError("Should be an object!");
    }

    return jsonObject
  }

  /**
   * Check if the imported object is created for the same site extension or not.
   * @param importedObject Object to check.
   */
  static checkIsSameSiteImportedObject(importedObject: Record<string, any>): SameSiteStatus {
    if (!('$site' in importedObject)) {
      return "unknown";
    }

    return importedObject.$site === __CURRENT_SITE__
      ? "same"
      : "different";
  }
}
