import {
  Body,
  Controller,
  Delete,
  Example,
  Post,
  Put,
  Request,
  Response,
  Route,
  Security,
  SuccessResponse,
  Tags,
} from "tsoa";

import minioClient from "../minio";
import esClient from "../elasticsearch";

import HttpError from "../interfaces/http-error";
import HttpStatusCode from "../interfaces/http-status";
import { StorageFile, StorageFolder } from "../interfaces/storage-fs";

import { copyCond } from "../utils/minio";

import * as io from "../lib/websocket";

if (!process.env.MINIO_BUCKET) throw Error("Default MinIO bucket must be specified.");
if (!process.env.ELASTICSEARCH_INDEX) throw Error("Default ElasticSearch index must be specified.");

const DEFAULT_BUCKET = process.env.MINIO_BUCKET;
const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX;
const MINIO_ERROR_MESSAGE = "เกิดข้อผิดพลาดกับระบบจัดการไฟล์";
const PATH_NOT_FOUND_MESSAGE = "ไม่พบตำแหน่งที่ต้องการนำข้อมูลเข้า";
const PATH_ALREADY_EXIST = "ตำแหน่งดังกล่าวมีในระบบแล้ว";

interface ListRequestBody {
  operation: "folder" | "file";
  path: string[];
  hidden?: boolean;
}

interface FolderBody {
  /** @example ["แฟ้ม 1", "แฟ้ม 2"] */
  path: string[];
  /** @example "แฟ้ม 3" */
  name: string;
}

interface PutFolderBody {
  from: {
    /** @example ["แฟ้ม 1", "แฟ้ม 2"] */
    path: string[];
    /** @example "แฟ้ม 3" */
    name: string;
  };
  to: {
    /** @example ["แฟ้ม 1", "แฟ้ม 2"] */
    path: string[];
    /** @example "แฟ้ม 3 แก้ไข" */
    name: string;
  };
}

interface DeleteFolderBody {
  /** @example ["แฟ้ม 1", "แฟ้ม 2", "แฟ้ม 3 แก้ไข"] */
  path: string[];
}

interface FileBody {
  /** @example ["แฟ้ม 1", "แฟ้ม 2", "แฟ้ม 3"] */
  path: string[];
  /** @example "ไฟล์ 1.xlsx" */
  file: string;
  /** @example "การเงิน" */
  title?: string;
  /** @example "การเงิน" */
  description?: string;
  /** @example "นายก" */
  author?: string;
  /** @example ["การเงิน", "รายงาน"] */
  category?: string[];
  /** @example ["การเงิน", "รายรับ", "รายจ่าย"] */
  keyword?: string[];
  /** @example {} */
  metadata?: { [key: string]: unknown };
  /** @example false */
  hidden?: boolean;
}

interface PutFileBody extends Omit<FileBody, "file" | "path"> {
  /** หากต้องการอัพโหลดไฟล์ด้วยให้ส่งค่าเป็นจริง */
  from: {
    /** @example ["แฟ้ม 1", "แฟ้ม 2", "แฟ้ม 3"] */
    path: string[];
    /** @example "ไฟล์ 1.xlsx" */
    file: string;
  };
  to?: {
    /** @example ["แฟ้ม 1", "แฟ้ม 2", "แฟ้ม 3"] */
    path: string[];
    /** @example "ไฟล์ 1 แก้ไข.xlsx" */
    file: string;
  };
  /** @example false */
  upload?: boolean;
}

interface DeleteFileBody {
  /** @example ["แฟ้ม 1", "แฟ้ม 2", "แฟ้ม 3"] */
  path: string[];
  /** @example "ไฟล์ 1 แก้ไข.xlsx" */
  file: string;
}

interface DownloadFileBody {
  /** @example ["แฟ้ม 1", "แฟ้ม 2", "แฟ้ม 3"] */
  path: string[];
  /** @example "ไฟล์ 1 แก้ไข.xlsx" */
  file: string;
}

function stripLeadingSlash(str: string) {
  return str.replace(/^\//, "");
}

async function folderSize(path: string[]) {
  const size = await new Promise<number>((resolve, reject) => {
    const stream = minioClient.listObjectsV2(
      DEFAULT_BUCKET,
      path.length === 0 ? "" : path.join("/") + "/",
      true,
    );
    let total: number = 0;

    stream.on("data", (v) => {
      if (v && v.size) total += v.size;
    });
    stream.on("end", () => resolve(total));
    stream.on("error", () => reject(new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์")));
  });
  return size;
}

async function listFolder(path: string[], hidden: boolean = false) {
  const list = await new Promise<{ pathname: string; name: string }[]>((resolve, reject) => {
    const item: { pathname: string; name: string }[] = [];

    const stream = minioClient.listObjectsV2(
      DEFAULT_BUCKET,
      stripLeadingSlash(`${path.join("/")}/`),
    );
    stream.on("data", (v) => {
      if (v && v.prefix)
        item.push({
          pathname: v.prefix,
          name: v.prefix.split("/").filter(Boolean).at(-1)!,
        });
    });
    stream.on("end", () => resolve(item));
    stream.on("error", () => reject(new Error(MINIO_ERROR_MESSAGE)));
  });

  const folder = await Promise.all(
    list.map(async (v) => {
      if (v.name.startsWith(".") && !hidden) return undefined;

      // Get stat from hidden object that used to mark as folder as minio doesn't really have folder
      const stat = await minioClient
        .statObject(DEFAULT_BUCKET, `${v.pathname}.keep`)
        .catch((e) => console.error(`MinIO Error: ${e}`));

      if (!stat) return undefined;

      const { createdat, createdby } = stat.metaData;

      return {
        ...v,
        createdAt: createdat ?? "n/a",
        createdBy: createdby ?? "n/a",
      } satisfies StorageFolder;
    }),
  );

  return folder.filter((v: StorageFolder | undefined): v is StorageFolder => !!v);
}

async function listFile(path: string[], hidden: boolean = false) {
  const result = await esClient
    .search<StorageFile & { attachment: Record<string, string> }>({
      index: DEFAULT_INDEX,
      sort: [{ pathname: "asc" }],
      query: {
        bool: {
          must: { match: { path: stripLeadingSlash(`${path.join("/")}/`) } },
          must_not: !hidden ? { match: { hidden: true } } : undefined,
        },
      },
      size: 10000,
    })
    .then((r) => r.hits.hits);

  const records = result
    .map((v) => {
      if (v._source) {
        const { attachment, ...rest } = v._source;
        return rest satisfies StorageFile;
      }
    })
    .filter((v: StorageFile | undefined): v is StorageFile => !!v);

  return records;
}

async function checkPathExist(bucket: string, path: string[]) {
  if (path.filter(Boolean).length === 0) return true; // root does not contain any mark
  return await checkFileExist(bucket, `${path.filter(Boolean).join("/")}/.keep`);
}

async function checkFileExist(bucket: string, pathname: string) {
  return Boolean(
    await minioClient.statObject(bucket, stripLeadingSlash(pathname)).catch((e) => {
      if (e.code === "NotFound") return false;
      console.error(`Storage Error: ${e}`);
      throw new Error(MINIO_ERROR_MESSAGE);
    }),
  );
}

@Route("storage")
export class StorageController extends Controller {
  /**
   * @summary แสดงรายการแฟ้มหรือไฟล์
   */
  @Post("list")
  @Tags("Storage Folder", "Storage File")
  @Security("bearerAuth")
  @Example([
    {
      path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/",
      name: "แฟ้ม 1",
      createdAt: "2021-07-20T12:33:13.018Z",
      createdBy: "admin",
    },
    {
      path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 2/",
      name: "แฟ้ม 2",
      createdAt: "2022-01-23T16:05:02.114Z",
      createdBy: "admin",
    },
  ])
  @Example([
    {
      pathname: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/เอกสาร 1.pdf",
      path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/",
      title: "เอกสาร",
      description: "เอกสารการเงิน",
      author: "นายก",
      category: ["บัญชี"],
      keyword: ["เงิน", "บัญชี", "รายจ่าย", "รายรับ"],
      upload: false,
      hidden: false,
      fileName: "เอกสาร 1.pdf",
      fileSize: 10240,
      fileType: "application/pdf",
      createdAt: "2021-07-20T12:33:13.018Z",
      createdBy: "admin",
      updatedAt: "2021-07-20T12:33:13.018Z",
      updatedBy: "admin",
    },
  ])
  @SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
  public async getList(@Body() body: ListRequestBody) {
    const path = body.path.filter(Boolean);

    if (!(await checkPathExist(DEFAULT_BUCKET, path))) {
      throw new HttpError(HttpStatusCode.NOT_FOUND, PATH_NOT_FOUND_MESSAGE);
    }

    if (body.operation === "folder") return await listFolder(path, body.hidden);
    if (body.operation === "file") return await listFile(path, body.hidden);
  }

  /**
   * @summary สร้างแฟ้ม
   */
  @Post("folder")
  @Tags("Storage Folder")
  @Security("bearerAuth", ["management-role", "admin"])
  @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
  public async postFolder(
    @Request() request: { user: { preferred_username: string } },
    @Body() body: FolderBody,
  ) {
    const { path, name } = body;

    if (!(await checkPathExist(DEFAULT_BUCKET, path))) {
      throw new HttpError(HttpStatusCode.NOT_FOUND, PATH_NOT_FOUND_MESSAGE);
    }

    const meta = {
      createdAt: new Date().toISOString(),
      createdBy: request.user.preferred_username,
    };

    const created = await minioClient
      .putObject(
        DEFAULT_BUCKET,
        stripLeadingSlash(`${path.join("/")}/${name.replace(/[/\\?%*:|"<>#]/g, "-").trim()}/.keep`),
        "",
        0,
        meta,
      )
      .catch((e) => console.error(`MinIO Error: ${e}`));

    if (!created) throw new Error(MINIO_ERROR_MESSAGE);

    !name.startsWith(".") &&
      io.getInstance()?.emit("FolderCreate", {
        pathname: stripLeadingSlash(
          `${path.join("/")}/${name.replace(/[/\\?%*:|"<>#]/g, "-").trim()}/`,
        ),
        name: name.replace(/[/\\?%*:|"<>#]/g, "-").trim(),
        ...meta,
      });

    return this.setStatus(HttpStatusCode.NO_CONTENT);
  }

  /**
   * ย้ายแฟ้มภายในตำแหน่งหนึ่งไปภายในอีกแฟ้มหนึ่ง
   * หรือเปลี่ยนชื่อได้
   *
   * @summary ย้ายแฟ้ม
   */
  @Put("folder")
  @Tags("Storage Folder")
  @Security("bearerAuth", ["management-role", "admin"])
  @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
  public async moveFolder(@Body() body: PutFolderBody) {
    const src = stripLeadingSlash(`${body.from.path.join("/")}/${body.from.name}`);
    const dst = stripLeadingSlash(`${body.to.path.join("/")}/${body.to.name}`);

    if (!(await checkPathExist(DEFAULT_BUCKET, src.split("/")))) {
      throw new HttpError(HttpStatusCode.NOT_FOUND, PATH_NOT_FOUND_MESSAGE);
    }
    if (await checkPathExist(DEFAULT_BUCKET, dst.split("/"))) {
      throw new HttpError(HttpStatusCode.CONFLICT, PATH_ALREADY_EXIST);
    }
    if (!(await checkPathExist(DEFAULT_BUCKET, body.to.path))) {
      throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "ไม่พบตำแหน่งที่ต้องการย้าย");
    }

    const list = await new Promise<{ pathname: string }[]>((resolve, reject) => {
      const stream = minioClient.listObjectsV2(DEFAULT_BUCKET, `${src}/`, true);
      const item: { pathname: string }[] = [];

      stream.on("data", (v) => {
        if (v && v.name) item.push({ pathname: v.name });
      });
      stream.on("end", () => resolve(item));
      stream.on("error", () => reject(new Error(MINIO_ERROR_MESSAGE)));
    });

    await Promise.all(
      list.map(async (v) => {
        const from = `/${DEFAULT_BUCKET}/${v.pathname}`;
        const to = stripLeadingSlash(`${dst}/${v.pathname.slice(`${src}/`.length)}`);

        const result = await minioClient
          .copyObject(DEFAULT_BUCKET, to, from, copyCond)
          .catch((e) => {
            console.error(`MinIO Error: ${e}`);
            throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้");
          });

        if (result) {
          if (v.pathname.includes(".keep")) {
            return await minioClient
              .removeObject(DEFAULT_BUCKET, v.pathname)
              .catch((e) => console.error(`MinIO Error: ${e}`));
          }

          const search = await esClient.search<
            StorageFile & { attachment: Record<string, string> }
          >({
            index: DEFAULT_INDEX!,
            query: { match: { pathname: v.pathname } },
          });

          if (search && search.hits.hits.length === 0) {
            await minioClient
              .removeObject(DEFAULT_BUCKET, v.pathname)
              .catch((e) => console.error(`MinIO Error: ${e}`));
            return console.log(
              `Trying to update file in storage but not exist in database: ${from} > ${to}`,
            );
          }

          const data = search.hits.hits[0];

          await esClient
            .update({
              index: DEFAULT_INDEX!,
              id: data._id,
              doc: {
                pathname: to,
                path: stripLeadingSlash(`${to.split("/").slice(0, -1).join("/")}/`),
              },
              refresh: "wait_for",
            })
            .catch((e) => console.error(`ElasticSearch Error: ${e}`));

          await minioClient
            .removeObject(DEFAULT_BUCKET, v.pathname)
            .catch((e) => console.error(`MinIO Error: ${e}`));
        }
      }),
    );

    io.getInstance()?.emit("FolderMove", {
      from: stripLeadingSlash(`${src}/`),
      to: stripLeadingSlash(`${dst}/`),
    });

    return this.setStatus(HttpStatusCode.NO_CONTENT);
  }

  /**
   * @summary ร้องขอขนาดของแฟ้ม มีหน่อยเป็นไบต์
   */
  @Post("folder/size")
  @Tags("Storage Folder")
  @Security("bearerAuth")
  @SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
  @Example({
    size: 10240,
  })
  public async folderSize(@Body() body: FolderBody) {
    return { size: await folderSize([...body.path, body.name]) };
  }

  /**
   * ลบแฟ้มออกจากระบบ
   * @summary ลบแฟ้มออกจากระบบ
   */
  @Delete("folder")
  @Tags("Storage Folder")
  @Security("bearerAuth", ["management-role", "admin"])
  @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
  public async deleteStorage(@Body() body: DeleteFolderBody) {
    if (body.path.length === 0) {
      throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่สามารถลบได้");
    }

    await new Promise<void>((resolve, reject) => {
      const objects: string[] = [];
      const stream = minioClient.listObjectsV2(
        DEFAULT_BUCKET,
        stripLeadingSlash(`${body.path.join("/")}/`),
        true,
      );

      stream.on("data", (v) => v && v.name && objects.push(v.name));
      stream.on("close", async () => {
        resolve(await minioClient.removeObjects(DEFAULT_BUCKET, objects));
      });
      stream.on("error", () => reject(new Error(MINIO_ERROR_MESSAGE)));
    });

    // delete file that is not uploaded
    await esClient.deleteByQuery({
      index: DEFAULT_INDEX,
      query: {
        bool: {
          must: [
            {
              prefix: {
                pathname: stripLeadingSlash(`${body.path.join("/")}/`),
              },
            },
            { match: { upload: false } },
          ],
        },
      },
    });

    io.getInstance()?.emit("FolderDelete", { pathname: body.path.join("/") + "/" });

    return this.setStatus(HttpStatusCode.NO_CONTENT);
  }

  /**
   * ร้องขอการอัปโหลดไฟล์ โดยเมื่อร้องขอจะได้ URL สำหรับอัพโหลดไฟล์มาพร้อมข้อมูลของไฟล์ที่จะทำการอัพโหลด
   *
   * @summary ร้องขอการอัปโหลดไฟล์
   */
  @Post("file")
  @Tags("Storage File")
  @Security("bearerAuth", ["management-role", "admin"])
  @SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
  @Example({
    pathname: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/เอกสาร 1.pdf",
    path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/",
    title: "เอกสาร",
    description: "เอกสารการเงิน",
    author: "นายก",
    category: ["บัญชี"],
    keyword: ["เงิน", "บัญชี", "รายจ่าย", "รายรับ"],
    upload: false,
    hidden: false,
    fileName: "เอกสาร 1.pdf",
    fileSize: 10240,
    fileType: "application/pdf",
    createdAt: "2021-07-20T12:33:13.018Z",
    createdBy: "admin",
    updatedAt: "2021-07-20T12:33:13.018Z",
    updatedBy: "admin",
    uploadUrl: "s3.storage.upload",
  })
  public async postFile(
    @Request() request: { user: { preferred_username: string } },
    @Body() body: FileBody,
  ) {
    const { path, file } = body;
    const validFileName = file.replace(/[/\\?%*:|"<>#]/g, "-").trim();

    if (!(await checkPathExist(DEFAULT_BUCKET, path))) {
      throw new HttpError(HttpStatusCode.NOT_FOUND, PATH_NOT_FOUND_MESSAGE);
    }

    const result = await esClient
      .search<StorageFile & { attachment?: Record<string, unknown> }>({
        index: DEFAULT_INDEX!,
        query: {
          match: {
            pathname: stripLeadingSlash(`${path.join("/")}/${validFileName}`),
          },
        },
      })
      .catch((e) => console.error(`MinIO Error: ${e}`));

    const metadata: StorageFile = {
      path: stripLeadingSlash(`${path.join("/")}/`),
      pathname: stripLeadingSlash(`${path.join("/")}/${validFileName}`),
      fileName: validFileName,
      fileSize: 0, // Will be get by minio object storage after file is uploaded
      fileType: "", // Will be determined by minio object storage after file is uploaded
      title: body.title ?? validFileName, // default to same as filename
      author: body.author ?? "",
      description: body.description ?? "",
      category: body.category ?? [],
      keyword: body.keyword ?? [],
      metadata: body.metadata ?? {},
      upload: false, // flag
      hidden: body.hidden ?? false,
      createdAt: new Date().toISOString(),
      createdBy: request.user.preferred_username,
      updatedAt: new Date().toISOString(),
      updatedBy: request.user.preferred_username,
    };

    // Pathname is unique and should not have multiple record with same path
    if (result && result.hits.hits.length > 0 && result.hits.hits[0]._source) {
      await esClient
        .delete({
          index: DEFAULT_INDEX!,
          id: result.hits.hits[0]._id,
        })
        .catch((e) => console.error(`MinIO Error: ${e}`));

      const rec = result.hits.hits[0]._source;

      // File exist get created at and created by field but all other will be replaced
      // by provided infomation or blank if not provided
      metadata.createdAt = rec.createdAt;
      metadata.createdBy = rec.createdBy;
    }

    await esClient.index({
      index: DEFAULT_INDEX,
      document: metadata,
      refresh: "wait_for", // Must have or else it doesn't wait for updated index resulted in data not found on fetch
    });

    !metadata.hidden && io.getInstance()?.emit("FileUploadRequest", metadata);

    const presignedUrl = await minioClient.presignedPutObject(DEFAULT_BUCKET, metadata.pathname);

    return { ...metadata, uploadUrl: presignedUrl };
  }

  /**
   * @summary ย้ายไฟล์หรือเปลี่ยนชื่อไฟล์
   */
  @Put("file")
  @Tags("Storage File")
  @Security("bearerAuth", ["management-role", "admin"])
  @SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
  @Response(HttpStatusCode.NO_CONTENT, "สำเร็จ")
  @Example({ uploadUrl: "s3.storage.upload" })
  public async moveFile(
    @Request() request: { user: { preferred_username: string } },
    @Body() body: PutFileBody,
  ) {
    const search = await esClient
      .search<StorageFile & { attachment: Record<string, any> }>({
        index: DEFAULT_INDEX,
        query: {
          match: {
            pathname: stripLeadingSlash(`${body.from.path.join("/")}/${body.from.file}`),
          },
        },
      })
      .catch((e) => console.error(`ElasticSearch Error: ${e}`));

    if (!search) {
      throw new Error("เกิดข้อผิดพลาดกับระบบฐานข้อมูล กรุณาลองใหม่ในภายหลัง");
    }
    if (search && search.hits.hits.length === 0) {
      throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบไฟล์ดังกล่าว");
    }
    if (
      !(await checkFileExist(
        DEFAULT_BUCKET,
        stripLeadingSlash(`${body.from.path.join("/")}/${body.from.file}`),
      ))
    ) {
      await esClient.delete({
        index: DEFAULT_INDEX,
        id: search.hits.hits[0]._id,
      });
      throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบไฟล์ดังกล่าว");
    }
    if (body.to && JSON.stringify(body.from) !== JSON.stringify(body.to)) {
      if (!(await checkPathExist(DEFAULT_BUCKET, body.to.path))) {
        throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "ไม่พบตำแหน่งที่ต้องการย้าย");
      }
      if (
        await checkFileExist(
          DEFAULT_BUCKET,
          stripLeadingSlash(`${body.to.path.join("/")}/${body.to.file}`),
        )
      ) {
        throw new HttpError(
          HttpStatusCode.PRECONDITION_FAILED,
          "พบไฟล์ในต้ำแหน่งปลายทาง ไม่สามารถย้ายได้",
        );
      }
    }
    if (!search.hits.hits[0]._source) {
      // This should not possible.
      // Just in case the result found with no associated data.
      await esClient.delete({
        index: DEFAULT_INDEX,
        id: search.hits.hits[0]._id,
      });
      throw new Error("ไม่พบข้อมูลในฐานข้อมูล");
    }

    const id = search.hits.hits[0]._id;
    const { attachment: _, ...source } = search.hits.hits[0]._source;

    const { to, from, upload, ...metadata } = body;

    const dateMeta = {
      updatedAt: new Date().toISOString(),
      updatedBy: request.user.preferred_username,
    };

    if (from && to && JSON.stringify(from) !== JSON.stringify(to)) {
      const src = [DEFAULT_BUCKET, ...from.path, ""].join("/") + from.file;
      const dst = stripLeadingSlash(`${to.path.join("/")}/${to.file}`);

      const result = await minioClient.copyObject(DEFAULT_BUCKET, dst, src, copyCond).catch((e) => {
        console.error(`MinIO Error: ${e}`);
        throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้");
      });

      if (result) {
        await esClient
          .update({
            index: DEFAULT_INDEX,
            id: id,
            doc: {
              ...metadata,
              metadata: metadata["metadata"]
                ? {
                    ...source["metadata"], // keep old field
                    ...metadata["metadata"], // replace some field that user update
                  }
                : undefined,
              path: stripLeadingSlash(`${to.path.join("/")}/`),
              pathname: dst,
              fileName: to.file,
              ...dateMeta,
            },
            refresh: "wait_for",
          })
          .then(
            async () =>
              await minioClient.removeObject(
                DEFAULT_BUCKET,
                stripLeadingSlash(`${from.path.join("/")}/${from.file}`),
              ),
          )
          .catch((e) => console.error(`ElasticSearch Error: ${e}`));

        if (!source.hidden && !metadata.hidden) {
          io.getInstance()?.emit("FileMove", {
            from: source,
            to: {
              ...source,
              ...metadata,
              path: stripLeadingSlash(`${to.path.join("/")}/`),
              pathname: dst,
              fileName: to.file,
              ...dateMeta,
            },
          });
        }

        if (upload) {
          const presignedUrl = await minioClient.presignedPutObject(DEFAULT_BUCKET, dst);
          return { uploadUrl: presignedUrl };
        }
        return this.setStatus(HttpStatusCode.NO_CONTENT);
      }
    }

    if (from) {
      await esClient
        .update({
          index: DEFAULT_INDEX,
          id: id,
          doc: {
            ...metadata,
            ...dateMeta,
          },
          refresh: "wait_for",
        })
        .catch((e) => console.error(`ElasticSearch Error: ${e}`));

      if (!source.hidden && !metadata.hidden) {
        io.getInstance()?.emit("FileMove", {
          from: source,
          to: {
            ...source,
            ...metadata,
            ...dateMeta,
          },
        });
      }

      if (upload) {
        const src = stripLeadingSlash(`${from.path.join("/")}/${from.file}`);
        const presignedUrl = await minioClient.presignedPutObject(DEFAULT_BUCKET, src);
        return { uploadUrl: presignedUrl };
      }
    }

    return this.setStatus(HttpStatusCode.NO_CONTENT);
  }

  /**
   * @summary ลบไฟล์ออกจากระบบ
   */
  @Delete("file")
  @Tags("Storage File")
  @Security("bearerAuth", ["management-role", "admin"])
  @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
  public async deleteFile(@Body() body: DeleteFileBody) {
    const pathname = stripLeadingSlash(`${body.path.join("/")}/${body.file}`);

    await minioClient
      .removeObject(DEFAULT_BUCKET, pathname)
      .catch((e) => console.error(`MinIO Error: ${e}`));
    await esClient
      .deleteByQuery({
        index: DEFAULT_INDEX,
        query: { match: { pathname } },
      })
      .catch((e) => console.error(`ElasticSearch Error: ${e}`));

    io.getInstance()?.emit("FileDelete", { pathname });

    return this.setStatus(HttpStatusCode.NO_CONTENT);
  }

  /**
   * @summary ขอข้อมูลการโหลดไฟล์
   */
  @Post("file/download")
  @Tags("Download")
  @Security("bearerAuth", ["management-role", "admin"])
  @SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
  @Example({
    pathname: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/เอกสาร 1.pdf",
    path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/",
    title: "เอกสาร",
    description: "เอกสารการเงิน",
    category: ["บัญชี"],
    keyword: ["เงิน", "บัญชี", "รายจ่าย", "รายรับ"],
    author: "นายก",
    upload: false,
    hidden: false,
    fileName: "เอกสาร 1.pdf",
    fileSize: 10240,
    fileType: "application/pdf",
    createdAt: "2021-07-20T12:33:13.018Z",
    createdBy: "admin",
    updatedAt: "2021-07-20T12:33:13.018Z",
    updatedBy: "admin",
    downloadUrl: "s3.storage.download",
  })
  public async downloadFile(@Body() body: DownloadFileBody) {
    const pathname = stripLeadingSlash(`${body.path.join("/")}/${body.file}`);

    const search = await esClient.search<StorageFile & { attachment: Record<string, string> }>({
      index: DEFAULT_INDEX,
      query: {
        match: { pathname },
      },
    });

    if (search && search.hits.hits.length === 0) {
      throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบไฟล์");
    }

    const { attachment, ...rest } = search.hits.hits[0]._source!;

    return {
      ...rest,
      downloadUrl: await minioClient.presignedGetObject(DEFAULT_BUCKET, pathname),
    };
  }
}
