import { Sequelize, WhereOptions, literal, Op } from "sequelize";
import Models from "../models";
import { AppError } from "../../utils/errors";
import { Common } from "../../utils/common";

const attachmentAttributes: AttributeElement[] = [
    "id",
    "thumbnails",
    "dataKey",
    "size",
    "filePath",
    "uniqueName",
    "fileName",
    "extension",
    "type",
    "status",
    "createdAt",
    "updatedAt",
];
const authorAttributes: AttributeElement[] = [
    "id",
    "onlineStatus",
    [literal("`author->userProfile`.`name`"), "name"],
    [
        literal(`(
              SELECT JSON_OBJECT(
                'id', a.id,
                'fileName', a.file_name,
                'filePath', CONCAT('${process.env.PROTOCOL}://', '${process.env.API_HOST}', '/attachment/', a.unique_name),
                'cdnUrl', CONCAT('${process.env.CDN_PATH}', '/attachment/', a.unique_name)
              )
              FROM user_profile as up
              JOIN attachments as a ON up.profile_image_id = a.id
              WHERE up.user_id = author.id
              LIMIT 1
            )`),
        "profileImage",
    ],
];

export class AttachmentDao {
    private accountId: number | null;
    private userId: number | null;
    private language: string;
    private scope: string[] | null;
    private config: userConfig | null;
    constructor(
        private options: {
            language: string;
            scope: string[] | null;
            accountId: number | null;
            userId: number | null;
            config: userConfig | null;
        } = {
            language: process.env.DEFAULT_LANGUAGE_CODE!,
            scope: null,
            config: null,
            userId: null,
            accountId: null,
        }
    ) {
        this.language = options.language;
        this.scope = options.scope ?? [];
        this.config = options.config ?? null;
        this.userId = options.userId ?? null;
        this.accountId = options.accountId ?? null;
    }

    // get association for the entity
    private includeAssociations = (expended: boolean): IncludeOption[] => {
        const includeModels: IncludeOption[] = [];
        if (expended) {
            includeModels.push({
                attributes: authorAttributes,
                model: Models.User,
                as: "author",
                include: [{ attributes: [], model: Models.UserProfile, as: "userProfile", include: [{ model: Models.Attachment, as: "profileImage" }] }],
            });
        }
        return includeModels;
    };

    getAttachment = async (data: AttachmentGetDaoInput, options: DaoOptions = {}): Promise<attachmentObjectInteface> => {
        try {
            const { id = null, uniqueName = null, fullObject, paranoid = true } = data;
            const attachment = await Models.Attachment.findOne({
                attributes: attachmentAttributes,
                where: { ...(id ? { id } : uniqueName ? { uniqueName } : {}), accountId: { [Op.or]: [null, this.accountId] } },
                include: this.includeAssociations(fullObject),
                paranoid: paranoid,
                transaction: options.transaction,
            });
            if (attachment) {
                return JSON.parse(JSON.stringify(attachment)) as unknown as attachmentObjectInteface;
            }
            throw new AppError(404, "FILE_NOT_FOUND", {});
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_WITH_DAO", err);
        }
    };

    getAttachments = async (data: AttachmentGetAllDaoInput, options: DaoOptions = {}): Promise<attachmentObjectInteface[]> => {
        try {
            const { ids = null, uniqueNames = null, fullObject, paranoid = true } = data;
            const attachments = await Models.Attachment.findAll({
                attributes: attachmentAttributes,
                where: { ...(ids ? { id: ids } : uniqueNames ? { uniqueName: uniqueNames } : {}), accountId: { [Op.or]: [null, this.accountId] } },
                include: this.includeAssociations(fullObject),
                paranoid: paranoid,
                transaction: options.transaction,
            });
            return JSON.parse(JSON.stringify(attachments)) as unknown as attachmentObjectInteface[];
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_WITH_DAO", err);
        }
    };

    // Build order by clause
    private buildOrderBy = (field: string, direction: string = "desc") => {
        switch (field) {
            case "sort-order":
                return [["sortOrder", direction]];
            case "size":
                return [[literal(`size`), direction]];
            case "id":
                return [["id", direction]];
            case "createdAt":
                return [[literal(`createdAt`), direction]];
            default:
                return [["id", "ASC"]];
        }
    };

    // Build filter
    private buildFilter = (where: WhereOptions & { [Op.and]: any[] }, searchText: string | null, status: boolean | null) => {
        let alphaString = "";
        let specialString = "";
        if (searchText) {
            let searchData: searchText = Common.prepareSearchText(searchText);
            if (searchData.specialString) {
                specialString = searchData.specialString;
            }
            if (searchData.alphaString) {
                alphaString = searchData.alphaString;
            }
            if (alphaString) {
                (where[Op.and] as any[]).push({
                    [Op.or]: [
                        { fileName: { [Op.like]: `%${alphaString}%` } },
                        { uniqueName: { [Op.like]: `%${alphaString}%` } },
                        { extension: { [Op.like]: `%${alphaString}%` } },
                    ],
                });
            }
            if (specialString) {
                (where[Op.and] as any[]).push({
                    [Op.or]: [
                        { fileName: { [Op.like]: `%${specialString}%` } },
                        { uniqueName: { [Op.like]: `%${specialString}%` } },
                        { extension: { [Op.like]: `%${specialString}%` } },
                    ],
                });
            }
        }
        if (status != null) {
            where = { ...where, status: status };
        }
        return { where: where, replacements: { alphaString: alphaString, specialString: specialString } };
    };

    // list attachments with pagination
    getAllAttachments = async (data: AttachmentGetAllAttachmentsDaoInput, options: DaoOptions = {}): Promise<AttachmentPaginatedData> => {
        try {
            const { listRequest, fullObject, paranoid = true } = data;
            const { page, perPage, searchText, status, sortBy, sortDirection } = listRequest;
            let offset = (page - 1) * perPage;
            let where: WhereOptions & { [Op.and]: any[] } = {
                accountId: { [Op.or]: [null, this.accountId] },
                [Op.and]: [],
            };
            let applyFilter = this.buildFilter(where, searchText, status);
            const orderBy = this.buildOrderBy(sortBy, sortDirection);
            const attachments = await Models.Attachment.findAndCountAll({
                attributes: attachmentAttributes,
                where: applyFilter.where,
                replacements: applyFilter.replacements,
                include: this.includeAssociations(fullObject),
                offset: offset,
                limit: perPage,
                paranoid: paranoid,
                subQuery: false,
                order: orderBy,
                transaction: options.transaction,
            });
            return JSON.parse(JSON.stringify(attachments)) as unknown as AttachmentPaginatedData;
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_WITH_DAO", err);
        }
    };

    saveAttachment = async (data: AttachmentSaveDaoInput, options: DaoOptions = {}): Promise<attachmentObjectInteface> => {
        try {
            let saveAttachment = await Models.Attachment.create(data.attachmentData, { transaction: options.transaction });
            const getAttachmentInput: AttachmentGetDaoInput = { id: saveAttachment.id, uniqueName: null, fullObject: true };
            let attachment: attachmentObjectInteface = await this.getAttachment(getAttachmentInput, options);
            return attachment;
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_WITH_DAO", err);
        }
    };

    getFileByName = async (data: AttachmentGetFileByNameDaoInput, options: DaoOptions = {}): Promise<attachmentObjectInteface> => {
        try {
            const getAttachmentInput: AttachmentGetDaoInput = {
                id: null,
                uniqueName: data.uniqueName,
                fullObject: data.fullObject ?? true,
                paranoid: data.paranoid ?? true,
            };
            let attachment: attachmentObjectInteface = await this.getAttachment(getAttachmentInput, options);
            return attachment;
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_WITH_DAO", err);
        }
    };

    getFileById = async (data: AttachmentGetFileByIdDaoInput, options: DaoOptions = {}): Promise<attachmentObjectInteface> => {
        try {
            const getAttachmentInput: AttachmentGetDaoInput = {
                id: data.id,
                uniqueName: null,
                fullObject: data.fullObject ?? true,
                paranoid: data.paranoid ?? true,
            };
            let attachment: attachmentObjectInteface = await this.getAttachment(getAttachmentInput, options);
            return attachment;
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_WITH_DAO", err);
        }
    };

    deleteFile = async (data: AttachmentDeleteDaoInput, options: DaoOptions = {}): Promise<attachmentObjectInteface> => {
        try {
            const getAttachmentInput: AttachmentGetDaoInput = {
                id: data.id,
                uniqueName: null,
                fullObject: data.fullObject ?? true,
                paranoid: data.paranoid ?? true,
            };
            let attachment: attachmentObjectInteface = await this.getAttachment(getAttachmentInput, options);
            if (attachment) {
                await Models.Attachment.destroy({
                    where: { id: data.id, accountId: { [Op.or]: [null, this.accountId] } },
                    transaction: options.transaction,
                });
                return attachment;
            } else {
                throw new AppError(404, "FILE_NOT_FOUND", { id: "FILE_NOT_FOUND" });
            }
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_WITH_DAO", err);
        }
    };

    verifyFiles = async (data: AttachmentVerifyFilesDaoInput, options: DaoOptions = {}): Promise<attachmentObjectInteface[]> => {
        try {
            const getAttachmentsInput: AttachmentGetAllDaoInput = {
                ids: data.ids,
                uniqueNames: null,
                fullObject: data.fullObject ?? true,
                paranoid: data.paranoid ?? true,
            };
            let attachments: attachmentObjectInteface[] = await this.getAttachments(getAttachmentsInput, options);
            return attachments;
        } catch (err) {
            if (err instanceof AppError) {
                throw err;
            }
            throw new AppError(500, "SOMETHING_WENT_WRONG_WITH_DAO", err);
        }
    };
}
