import type { FileListObject } from "media/storage/Storage";
import {
   type BaseModuleApiOptions,
   type FetchPromise,
   type ResponseObject,
   ModuleApi,
   type PrimaryFieldType,
   type TInput,
} from "modules/ModuleApi";
import type { ApiFetcher } from "Api";
import type { DB, FileUploadedEventData } from "bknd";

export type MediaApiOptions = BaseModuleApiOptions & {
   upload_fetcher: ApiFetcher;
   init?: RequestInit;
};

export class MediaApi extends ModuleApi<MediaApiOptions> {
   protected override getDefaultOptions(): Partial<MediaApiOptions> {
      return {
         basepath: "/api/media",
         upload_fetcher: fetch,
         init: {},
      };
   }

   listFiles() {
      return this.get<FileListObject[]>(["files"]);
   }

   getFile(filename: string) {
      return this.get<ReadableStream<Uint8Array>>(["file", filename], undefined, {
         headers: {
            Accept: "*/*",
         },
      });
   }

   async getFileStream(filename: string): Promise<ReadableStream<Uint8Array>> {
      const { res } = await this.getFile(filename);
      if (!res.ok || !res.body) {
         throw new Error("Failed to fetch file");
      }
      return res.body;
   }

   async download(filename: string): Promise<File> {
      const { res } = await this.getFile(filename);
      if (!res.ok || !res.body) {
         throw new Error("Failed to fetch file");
      }
      return (await res.blob()) as File;
   }

   getFileUploadUrl(file?: { path: string }): string {
      if (!file) return this.getUrl("/upload");
      return this.getUrl(`/upload/${file.path}`);
   }

   getEntityUploadUrl(entity: string, id: PrimaryFieldType, field: string) {
      return this.getUrl(`/entity/${entity}/${id}/${field}`);
   }

   getUploadHeaders(): Headers {
      if (this.options.token_transport === "header" && this.options.token) {
         return new Headers({
            Authorization: `Bearer ${this.options.token}`,
         });
      }
      return new Headers();
   }

   protected uploadFile<T extends FileUploadedEventData>(
      body: BodyInit,
      opts?: {
         filename?: string;
         path?: TInput;
         _init?: Omit<RequestInit, "body">;
         query?: Record<string, any>;
      },
   ): FetchPromise<ResponseObject<T>> {
      const headers = {
         "Content-Type": "application/octet-stream",
         ...(opts?._init?.headers || {}),
      };
      let name: string = opts?.filename || "";
      try {
         if (typeof (body as File).type !== "undefined") {
            headers["Content-Type"] = (body as File).type;
         }
         if (!opts?.filename) {
            name = (body as File).name;
         }
      } catch (e) {}

      if (name && name.length > 0 && name.includes("/")) {
         name = name.split("/").pop() || "";
      }

      const init = {
         ...this.options.init,
         ...(opts?._init || {}),
         headers,
      };
      if (opts?.path) {
         return this.request<T>(opts.path, opts?.query, {
            ...init,
            body,
            method: "POST",
         });
      }

      if (!name || name.length === 0) {
         throw new Error("Invalid filename");
      }

      return this.request<T>(opts?.path ?? ["upload", name], opts?.query, {
         ...init,
         body,
         method: "POST",
      });
   }

   async upload<T extends FileUploadedEventData>(
      item: Request | Response | string | File | Blob | ReadableStream | Buffer<ArrayBufferLike>,
      opts: {
         filename?: string;
         _init?: Omit<RequestInit, "body">;
         path?: TInput;
         fetcher?: ApiFetcher;
         query?: Record<string, any>;
      } = {},
   ) {
      if (item instanceof Request || typeof item === "string") {
         const fetcher = opts.fetcher ?? this.options.upload_fetcher;
         const res = await fetcher(item);
         if (!res.ok || !res.body) {
            throw new Error("Failed to fetch file");
         }
         return this.uploadFile<T>(res.body, opts);
      } else if (item instanceof Response) {
         if (!item.body) {
            throw new Error("Invalid response");
         }
         return this.uploadFile<T>(item.body, {
            ...(opts ?? {}),
            _init: {
               ...(opts._init ?? {}),
               headers: {
                  ...(opts._init?.headers ?? {}),
                  "Content-Type": item.headers.get("Content-Type") || "application/octet-stream",
               },
            },
         });
      }

      return this.uploadFile<T>(item as BodyInit, opts);
   }

   async uploadToEntity(
      entity: string,
      id: PrimaryFieldType,
      field: string,
      item: Request | Response | string | File | ReadableStream | Buffer<ArrayBufferLike>,
      opts?: {
         _init?: Omit<RequestInit, "body">;
         fetcher?: typeof fetch;
         overwrite?: boolean;
      },
   ): Promise<ResponseObject<FileUploadedEventData & { result: DB["media"] }>> {
      const query = opts?.overwrite !== undefined ? { overwrite: opts.overwrite } : undefined;
      return this.upload(item, {
         ...opts,
         path: ["entity", entity, id, field],
         query,
      });
   }

   deleteFile(filename: string) {
      return this.delete(["file", filename]);
   }
}
