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, replaceIllegalChars } from "../utils/minio";

import HttpStatusCode from "../interfaces/http-status";
import { StorageFile, StorageFolder } from "../interfaces/storage-fs";
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")
export class CabinetController extends Controller {
  @Get("/")
  @Tags("ตู้เอกสาร")
  @Security("bearerAuth")
  @Response(
    HttpStatusCode.INTERNAL_SERVER_ERROR,
    "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการตู้เอกสารได้ กรุณาลองใหม่ในภายหลัง",
  )
  @SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
  @Example([
    {
      path: "ตู้เอกสาร 1/",
      name: "ตู้เอกสาร 1",
      createdAt: "2021-07-20T12:33:13.018Z",
      createdBy: "admin",
    },
    {
      path: "ตู้เอกสาร 2/",
      name: "ตู้เอกสาร 2",
      createdAt: "2022-01-23T16:05:02.114Z",
      createdBy: "admin",
    },
  ])
  public async listCabinet(): Promise<StorageFolder[]> {
    const list = await listFolder(DEFAULT_BUCKET!).catch((e) =>
      console.error(`Error List Folder: ${e}`),
    );
    if (!list)
      throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการตู้เอกสารได้ กรุณาลองใหม่ในภายหลัง");
    return list;
  }

  @Post("/")
  @Tags("ตู้เอกสาร")
  @Security("bearerAuth", ["admin", "management-role"])
  @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์")
  @SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ")
  public async createCabinet(
    @Request() request: { user: { preferred_username: string } },
    @Body()
    body: {
      /**
       * @example "ตู้เอกสาร 1"
       */
      name: string;
    },
  ) {
    const meta = {
      createdAt: new Date().toISOString(),
      createdBy: request.user.preferred_username,
    };

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

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

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

    return this.setStatus(HttpStatusCode.CREATED);
  }

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

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

        const base = `${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}/`, to: `${replaceIllegalChars(body.name)}/` });

    return this.setStatus(HttpStatusCode.NO_CONTENT);
  }

  /**
   * @example cabinetName "ตู้เอกสาร 1"
   */
  @Delete("/{cabinetName}")
  @Tags("ตู้เอกสาร")
  @Security("bearerAuth", ["admin", "management-role"])
  @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้")
  @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
  public async deleteCabinet(@Path() cabinetName: string) {
    await new Promise<void>((resolve, reject) => {
      const objects: string[] = [];
      const stream = minioClient.listObjectsV2(DEFAULT_BUCKET!, `${cabinetName}/`, 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}/` });

    return this.setStatus(HttpStatusCode.NO_CONTENT);
  }

  /**
   * @example cabinetName "ตู้เอกสาร 1"
   */
  @Get("/{cabinetName}/size")
  @Tags("ตู้เอกสาร")
  @Security("bearerAuth")
  @SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
  public async calc(@Path() cabinetName: string) {
    const list = await listItem(DEFAULT_BUCKET!, `${cabinetName}/`, 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) };
  }
}
