import { set } from "lodash";
import { saveAs } from "file-saver";

interface ProviderElement<P extends unknown[], R> extends HTMLElement {
  args: P;

  updateArgs: (event: CustomEvent<Record<string, unknown>>) => void;

  updateArgsAndExecute: (
    event: CustomEvent<Record<string, unknown>>
  ) => Promise<R>;

  setArgs: (patch: Record<string, unknown>) => void;

  setArgsAndExecute: (patch: Record<string, unknown>) => Promise<R>;

  execute(): Promise<R>;

  executeWithArgs(...args: P): Promise<R>;

  saveAs(filename: string, ...args: P): Promise<void>;

  resolve(...args: P): R;
}

export function createProviderClass<T extends unknown[], U>(
  api: (...args: T) => U
): { new (): ProviderElement<T, U> } {
  return class extends HTMLElement {
    get $$typeof(): string {
      return "provider";
    }

    static get _dev_only_definedProperties(): string[] {
      return ["args"];
    }

    args = [] as T;

    updateArgs(event: CustomEvent<Record<string, unknown>>): void {
      if (!(event instanceof CustomEvent)) {
        // eslint-disable-next-line no-console
        console.warn(
          "`updateArgs/updateArgsAndExecute` is designed to receive an CustomEvent, if not, please use `setArgs/setArgsAndExecute` instead."
        );
      }
      this.setArgs(event.detail);
    }

    updateArgsAndExecute(
      event: CustomEvent<Record<string, unknown>>
    ): Promise<U> {
      this.updateArgs(event);
      return this.execute();
    }

    setArgs(patch: Record<string, unknown>): void {
      for (const [path, value] of Object.entries(patch)) {
        set(this.args, path, value);
      }
    }

    setArgsAndExecute(patch: Record<string, unknown>): Promise<U> {
      this.setArgs(patch);
      return this.execute();
    }

    execute(): Promise<U> {
      return this.executeWithArgs(...this.args);
    }

    async saveAs(filename: string, ...args: T): Promise<void> {
      const blob = await api(...args);
      saveAs((blob as unknown) as Blob, filename);
    }

    async executeWithArgs(...args: T): Promise<U> {
      try {
        const result = await api(...args);
        this.dispatchEvent(
          new CustomEvent("response.success", {
            detail: result,
          })
        );
        return result;
      } catch (error) {
        this.dispatchEvent(
          new CustomEvent("response.error", {
            detail: error,
          })
        );
        return Promise.reject(error);
      }
    }

    resolve(...args: T): U {
      return api(...args);
    }
  };
}