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

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

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

import { copyCond, pathExist, replaceIllegalChars } from "../utils/minio";
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}/file")
export class FileController extends Controller {
  /**
   * @example cabinetName "ตู้เอกสาร 1"
   * @example drawerName "ลิ้นชัก 1"
   * @example folderName "แฟ้ม 1"
   */
  @Get("/")
  @Tags("ไฟล์")
  @Security("bearerAuth")
  @SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
  @Example([
    {
      pathname: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/เอกสาร 1",
      path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/",
      title: "เอกสาร",
      description: "เอกสารการเงิน",
      category: ["บัญชี"],
      keyword: ["เงิน", "บัญชี", "รายจ่าย", "รายรับ"],
      upload: false,
      fileName: "เอกสาร 1",
      fileSize: 10240,
      fileType: "application/pdf",
      createdAt: "2021-07-20T12:33:13.018Z",
      createdBy: "admin",
      updatedAt: "2021-07-20T12:33:13.018Z",
      updatedBy: "admin",
    },
  ])
  public async getFile(
    @Path() cabinetName: string,
    @Path() drawerName: string,
    @Path() folderName: string,
  ): Promise<StorageFile[]> {
    const search = await esClient.search<StorageFile & { attachment: Record<string, string> }>({
      index: DEFAULT_INDEX!,
      query: {
        match: {
          path: `${cabinetName}/${drawerName}/${folderName}/`,
        },
      },
      size: 10000,
    });

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

    return records;
  }

  /**
   * @example cabinetName "ตู้เอกสาร 1"
   * @example drawerName "ลิ้นชัก 1"
   * @example folderName "แฟ้ม 1"
   */
  @Post("/")
  @Tags("ไฟล์")
  @Security("bearerAuth", ["admin", "management-role"])
  @Response(
    HttpStatusCode.NOT_FOUND,
    "ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ",
  )
  @SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ")
  @Example({
    pathname: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/เอกสาร 1",
    path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/",
    title: "เอกสาร",
    description: "เอกสารการเงิน",
    category: ["บัญชี"],
    keyword: ["เงิน", "บัญชี", "รายจ่าย", "รายรับ"],
    upload: false,
    fileName: "เอกสาร 1",
    fileSize: 10240,
    fileType: "application/pdf",
    createdAt: "2021-07-20T12:33:13.018Z",
    createdBy: "admin",
    updatedAt: "2021-07-20T12:33:13.018Z",
    updatedBy: "admin",
  })
  public async uploadFile(
    @Request() request: { user: { preferred_username: string } },
    @Body()
    body: {
      /**
       * @example "เอกสาร 1"
       */
      file: string;
      /**
       * @example "เอกสาร"
       */
      title?: string;
      /**
       * @example "เอกสารการเงิน"
       */
      description?: string;
      /**
       * @example ["บัญชี"]
       */
      category?: string[];
      /**
       * @example ["เงิน", "บัญชี", "รายจ่าย", "รายรับ"]
       */
      keyword?: string[];
    },
    @Path() cabinetName: string,
    @Path() drawerName: string,
    @Path() folderName: string,
  ) {
    if (body.file.length > 85) {
      throw new HttpError(HttpStatusCode.BAD_REQUEST, "ชื่อไฟล์ยาวเกินกำหนด");
    }

    const basePath = `${cabinetName}/${drawerName}/${folderName}/`;
    const pathname = `${basePath}${replaceIllegalChars(body.file)}`;

    if (!(await pathExist(DEFAULT_BUCKET!, basePath))) {
      throw new HttpError(
        HttpStatusCode.NOT_FOUND,
        "ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ",
      );
    }

    const result = await esClient
      .search<StorageFile & { attachment?: Record<string, unknown> }>({
        index: DEFAULT_INDEX!,
        query: { match: { pathname } },
      })
      .catch((e) => console.error(e));

    // 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(e));
    }

    const rec = result && result.hits.hits.length > 0 ? result.hits.hits[0]._source : false;

    const metadata: Partial<StorageFile> = {
      pathname,
      path: basePath,
      fileName: replaceIllegalChars(body.file),
      fileSize: 0,
      fileType: "",
      title: body.title ?? "",
      description: body.description ?? "",
      category: body.category ?? [],
      keyword: body.keyword ?? [],
      upload: false,
      createdAt: new Date().toISOString(),
      createdBy: rec ? rec.createdBy : "n/a",
      updatedAt: new Date().toISOString(),
      updatedBy: request.user.preferred_username ?? "n/a",
    };

    await esClient.index({
      index: DEFAULT_INDEX!,
      document: metadata,
      refresh: "wait_for",
    });

    const io = getInstance();
    io?.emit("FileUploadRequest", metadata);

    return {
      ...body,
      createdAt: metadata.createdAt,
      createdBy: metadata.createdBy,
      updatedAt: metadata.updatedAt,
      updatedBy: metadata.updatedBy,
      upload: await minioClient.presignedPutObject(DEFAULT_BUCKET!, pathname),
    };
  }

  /**
   * @example cabinetName "ตู้เอกสาร 1"
   * @example drawerName "ลิ้นชัก 1"
   * @example folderName "แฟ้ม 1"
   * @example fileName "เอกสาร 1"
   */
  @Patch("/{fileName}")
  @Tags("ไฟล์")
  @Security("bearerAuth", ["admin", "management-role"])
  @Response(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม")
  @Response(HttpStatusCode.NO_CONTENT, "สำเร็จ")
  @SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
  public async updateFile(
    @Request() request: { user: { preferred_username: string } },
    @Path() cabinetName: string,
    @Path() drawerName: string,
    @Path() folderName: string,
    @Path() fileName: string,
    @Body()
    body: {
      /**
       * @example "เอกสารใหม่"
       */
      file?: string;
      /**
       * @example "เอกสารการเงิน"
       */
      title?: string;
      /**
       * @example "เอกสารการเงินฉบับใหม่"
       */
      description?: string;
      /**
       * @example ["บัญชี"]
       */
      category?: string[];
      /**
       * @example ["เงิน", "บัญชี", "รายจ่าย", "รายรับ"]
       */
      keyword?: string[];
    },
  ): Promise<void | { upload: string }> {
    if (body.file && body.file.length > 85) {
      throw new HttpError(HttpStatusCode.BAD_REQUEST, "ชื่อไฟล์ยาวเกินกำหนด");
    }

    const basePath = `${cabinetName}/${drawerName}/${folderName}/`;
    const pathname = `${basePath}${fileName}`;

    if (
      !Boolean(
        await minioClient.statObject(DEFAULT_BUCKET!, `${pathname}`).catch((e) => {
          if (e.code === "NotFound") return false;
          throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์");
        }),
      )
    ) {
      throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบไฟล์");
    }

    const { file, ...metadata } = body;

    // assume user will probably replace file by re-upload but maybe just rename
    if (file) {
      const destination = `${basePath}${replaceIllegalChars(file)}`;
      const source = `/${DEFAULT_BUCKET}/${basePath}${fileName}`;
      const copy = await minioClient.copyObject(DEFAULT_BUCKET!, destination, source, copyCond);

      if (copy) {
        const search = await esClient
          .search<StorageFile & { attachment?: Record<string, unknown> }>({
            index: DEFAULT_INDEX!,
            query: { match: { pathname } },
          })
          .catch((e) => console.error(e));

        if (search && search.hits.hits.length > 0 && search.hits.hits[0]._source) {
          const { _index: index, _id: id } = search.hits.hits[0];
          const meta = {
            updatedAt: new Date().toISOString(),
            updatedBy: request.user.preferred_username ?? "n/a",
          };
          await esClient
            .update({
              index,
              id,
              doc: {
                ...metadata,
                pathname: destination,
                path: basePath,
                fileName: replaceIllegalChars(file),
                ...meta,
              },
              refresh: "wait_for",
            })
            .then(() => minioClient.removeObject(DEFAULT_BUCKET!, pathname));

          const io = getInstance();
          io?.emit("FileUpdateMove", {
            from: search.hits.hits[0]._source,
            to: {
              ...search.hits.hits[0]._source,
              ...metadata,
              pathname: destination,
              path: basePath,
              fileName: replaceIllegalChars(file),
              ...meta,
            },
          });
        } else {
          await minioClient.removeObject(DEFAULT_BUCKET!, pathname);
        }
      }
    } else {
      const search = await esClient
        .search<StorageFile & { attachment?: Record<string, unknown> }>({
          index: DEFAULT_INDEX!,
          query: { match: { pathname } },
        })
        .catch((e) => console.error(e));

      if (search && search.hits.hits.length > 0 && search.hits.hits[0]._source) {
        const { _index: index, _id: id } = search.hits.hits[0];
        const meta = {
          updatedAt: new Date().toISOString(),
          updatedBy: request.user.preferred_username ?? "n/a",
        };
        await esClient.update({
          index,
          id,
          doc: { ...metadata, ...meta },
          refresh: "wait_for",
        });

        const updated: StorageFile = {
          ...search.hits.hits[0]._source,
          ...metadata,
          ...meta,
        };

        const io = getInstance();
        io?.emit("FileUpdate", updated);
      }
    }

    return body.file
      ? {
          upload: await minioClient.presignedPutObject(
            DEFAULT_BUCKET!,
            `${basePath}${replaceIllegalChars(body.file) ?? fileName}`,
          ),
        }
      : this.setStatus(HttpStatusCode.NO_CONTENT);
  }

  /**
   * @example cabinetName "ตู้เอกสาร 1"
   * @example drawerName "ลิ้นชัก 1"
   * @example folderName "แฟ้ม 1"
   * @example fileName "เอกสารใหม่"
   */
  @Delete("/{fileName}")
  @Tags("ไฟล์")
  @Security("bearerAuth", ["admin", "management-role"])
  @SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
  public async deleteFile(
    @Path() cabinetName: string,
    @Path() drawerName: string,
    @Path() folderName: string,
    @Path() fileName: string,
  ) {
    await minioClient.removeObject(
      DEFAULT_BUCKET!,
      `${cabinetName}/${drawerName}/${folderName}/${fileName}`,
    );
    await esClient.deleteByQuery({
      index: DEFAULT_INDEX!,
      query: {
        match: { pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}` },
      },
    });
    return this.setStatus(HttpStatusCode.NO_CONTENT);
  }

  /**
   * @example cabinetName "ตู้เอกสาร 1"
   * @example drawerName "ลิ้นชัก 1"
   * @example folderName "แฟ้ม 1"
   * @example fileName "เอกสารใหม่"
   */
  @Get("/{fileName}")
  @Tags("ดาวน์โหลด")
  @Security("bearerAuth")
  @SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
  public async downloadFile(
    @Path() cabinetName: string,
    @Path() drawerName: string,
    @Path() folderName: string,
    @Path() fileName: string,
  ) {
    const search = await esClient.search<StorageFile & { attachment: Record<string, string> }>({
      index: DEFAULT_INDEX!,
      query: {
        match: { pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}` },
      },
    });

    if (search && search.hits.hits.length === 0) {
      throw new HttpError(HttpStatusCode.NOT_FOUND, "Not found");
    }

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

    return {
      ...rest,
      download: await minioClient.presignedGetObject(
        DEFAULT_BUCKET!,
        `${cabinetName}/${drawerName}/${folderName}/${fileName}`,
      ),
    };
  }
}
