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

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

import { copyCond, listFolder, listItem, pathExist, replaceIllegalChars } from "../utils/minio";

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

const DEFAULT_BUCKET = process.env.MINIO_BUCKET;
const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX;

if (!DEFAULT_BUCKET) throw Error("Default MinIO bucket must be specified.");
if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified.");

@Route("/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder")
export class SubFolderController extends Controller {
  /**
   * @example cabinetName "ตู้เอกสาร 1"
   * @example drawerName "ลิ้นชัก 1"
   * @example folderName "แฟ้ม 1"
   */
  @Get("/")
  @Tags("แฟ้มย่อย")
  @Security("bearerAuth")
  @Response(
    HttpStatusCode.INTERNAL_SERVER_ERROR,
    "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง",
  )
  @SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
  @Example([
    {
      path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/แฟ้มย่อย 1",
      name: "แฟ้มย่อย 1",
      createdAt: "2021-07-20T12:33:13.018Z",
      createdBy: "admin",
    },
    {
      path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/แฟ้มย่อย 2",
      name: "แฟ้มย่อย 2",
      createdAt: "2022-01-23T16:05:02.114Z",
      createdBy: "admin",
    },
  ])
  public async listFolder(
    @Path() cabinetName: string,
    @Path() drawerName: string,
    @Path() folderName: string,
  ): Promise<StorageFolder[]> {
    const list = await listFolder(
      DEFAULT_BUCKET!,
      `${cabinetName}/${drawerName}/${folderName}`,
    ).catch((e) => console.error(`Error List Folder: ${e}`));
    if (!list) throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง");
    return list;
  }

  /**
   * @example cabinetName "ตู้เอกสาร 1"
   * @example drawerName "ลิ้นชัก 1"
   * @example folderName "แฟ้ม 1"
   */
  @Post("/")
  @Tags("แฟ้มย่อย")
  @Security("bearerAuth", ["admin", "management-role"])
  @Response(HttpStatusCode.NOT_FOUND, "ไม่พบของแฟ้ม")
  @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์")
  @SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ")
  public async createFolder(
    @Request() request: { user: { preferred_username: string } },
    @Body() body: { name: string },
    @Path() cabinetName: string,
    @Path() drawerName: string,
    @Path() folderName: string,
  ) {
    const basePath = `${cabinetName}/${drawerName}/${folderName}/`;

    if (!(await pathExist(DEFAULT_BUCKET!, basePath))) {
      throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างลิ้นชัก");
    }
    const meta = {
      createdAt: new Date().toISOString(),
      createdBy: request.user.preferred_username,
    };

    const created = await minioClient
      .putObject(DEFAULT_BUCKET!, `${basePath}${replaceIllegalChars(body.name)}/.keep`, "", 0, meta)
      .catch((e) => console.error(e));

    if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์");

    const io = getInstance();
    io?.emit("CreateFolder", {
      pathname: `${basePath}${replaceIllegalChars(body.name)}/`,
      name: replaceIllegalChars(body.name),
      ...meta,
    });

    return this.setStatus(HttpStatusCode.CREATED);
  }

  /**
   * @example cabinetName "ตู้เอกสาร 1"
   * @example drawerName "ลิ้นชัก 1"
   * @example folderName "แฟ้ม 1"
   * @example subFolderName "แฟ้มย่อย 1"
   */
  @Put("/{subFolderName}")
  @Tags("แฟ้มย่อย")
  @Security("bearerAuth", ["admin", "management-role"])
  @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้")
  @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
  public async editFolder(
    @Body()
    body: {
      /**
       * @example "แฟ้มใหม่"
       */
      name: string;
    },
    @Path() cabinetName: string,
    @Path() drawerName: string,
    @Path() folderName: string,
    @Path() subFolderName: string,
  ) {
    const path = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`;
    const list = await listItem(DEFAULT_BUCKET!, path, true);

    await Promise.all(
      list.map(async (current) => {
        if (!current.name) return;

        const base = `${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars(
          body.name,
        )}/`;
        const destination = `${base}${current.name.slice(path.length)}`;
        const source = `/${DEFAULT_BUCKET}/${current.name}`;

        return await minioClient
          .copyObject(DEFAULT_BUCKET!, destination, source, copyCond)
          .then(async () => {
            if (current.name.includes(".keep")) {
              return await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
            }

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

            if (search && search.hits.hits.length === 0) throw new Error("ไม่พบข้อมูลในฐานข้อมูล");

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

            await esClient.update({
              index: DEFAULT_INDEX!,
              id: data._id,
              doc: {
                pathname: destination,
                path: destination.split("/").slice(0, -1).join("/") + "/",
              },
              refresh: "wait_for",
            });

            await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
          })
          .catch((e) => {
            console.error(e);
            throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้");
          });
      }),
    );

    const io = getInstance();
    io?.emit("EditFolder", {
      from: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`,
      to: `${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars(body.name)}/`,
    });

    return this.setStatus(HttpStatusCode.NO_CONTENT);
  }

  /**
   * @example cabinetName "ตู้เอกสาร 1"
   * @example drawerName "ลิ้นชัก 1"
   * @example folderName "แฟ้ม 1"
   * @example subFolderName "แฟ้มย่อย 1"
   */
  @Delete("/{subFolderName}")
  @Tags("แฟ้มย่อย")
  @Security("bearerAuth", ["admin", "management-role"])
  @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
  public async deleteFolder(
    @Path() cabinetName: string,
    @Path() drawerName: string,
    @Path() folderName: string,
    @Path() subFolderName: string,
  ) {
    await new Promise<void>((resolve, reject) => {
      const objects: string[] = [];
      const stream = minioClient.listObjectsV2(
        DEFAULT_BUCKET!,
        `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`,
        true,
      );

      stream.on("data", (v) => {
        if (v && v.name) objects.push(v.name);
      });
      stream.on("close", async () =>
        resolve(await minioClient.removeObjects(DEFAULT_BUCKET!, objects)),
      );
      stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้")));
    });

    const io = getInstance();
    io?.emit("DeleteFolder", {
      pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`,
    });

    return this.setStatus(HttpStatusCode.NO_CONTENT);
  }

  /**
   * @example cabinetName "ตู้เอกสาร 1"
   * @example drawerName "ลิ้นชัก 1"
   * @example folderName "แฟ้ม 1"
   * @example subFolderName "แฟ้มย่อย 1"
   */
  @Get("/{subFolderName}/size")
  @Tags("แฟ้มย่อย")
  @Security("bearerAuth")
  @SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
  public async calc(
    @Path() cabinetName: string,
    @Path() drawerName: string,
    @Path() folderName: string,
    @Path() subFolderName: string,
  ) {
    const list = await listItem(
      DEFAULT_BUCKET!,
      `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`,
      true,
    ).catch((e) => console.error(`Error List Folder: ${e}`));

    if (!list) throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง");

    return { size: list.reduce<number>((a, c) => a + c.size, 0) };
  }
}
