import FormData from "form-data";
import fs from "node:fs";
import { ZaloApiError } from "../Errors/ZaloApiError.js";
import { ThreadType } from "../models/index.js";
import { apiFactory, getFileExtension, getFileName, getFileSize, getImageMetaData, getMd5LargeFileObject, resolveResponse, } from "../utils.js";
const urlType = {
    image: "photo_original/upload",
    video: "asyncfile/upload",
    others: "asyncfile/upload",
};
export const uploadAttachmentFactory = apiFactory()((api, ctx, utils) => {
    const serviceURL = `${api.zpwServiceMap.file[0]}/api`;
    const { sharefile } = ctx.settings.features;
    function isExceedMaxFile(totalFile) {
        return totalFile > sharefile.max_file;
    }
    function isExceedMaxFileSize(fileSize) {
        return fileSize > sharefile.max_size_share_file_v3 * 1024 * 1024;
    }
    function isExtensionValid(ext) {
        return sharefile.restricted_ext_file.indexOf(ext) == -1;
    }
    /**
     * Upload an attachment to a thread
     *
     * @param sources path to files or attachment sources
     * @param threadId Group or User ID
     * @param type Message type (User or Group)
     *
     * @throws {ZaloApiError | ZaloApiMissingImageMetadataGetter}
     */
    return async function uploadAttachment(sources, threadId, type = ThreadType.User) {
        if (!sources)
            throw new ZaloApiError("Missing sources");
        if (!Array.isArray(sources))
            sources = [sources];
        if (sources.length == 0)
            throw new ZaloApiError("Missing sources");
        if (isExceedMaxFile(sources.length))
            throw new ZaloApiError("Exceed maximum file of " + sharefile.max_file);
        if (!threadId)
            throw new ZaloApiError("Missing threadId");
        const chunkSize = ctx.settings.features.sharefile.chunk_size_file;
        const isGroupMessage = type == ThreadType.Group;
        const attachmentsData = [];
        const url = `${serviceURL}/${isGroupMessage ? "group" : "message"}/`;
        const typeParam = isGroupMessage ? "11" : "2";
        let clientId = Date.now();
        for (const source of sources) {
            const isFilePath = typeof source == "string";
            const isBuffer = typeof source == "object" && source.data instanceof Buffer;
            if (!isFilePath && !isBuffer)
                throw new ZaloApiError("Invalid source type");
            if (!isFilePath && !source.filename)
                throw new ZaloApiError("Missing filename");
            if (isFilePath && !fs.existsSync(source))
                throw new ZaloApiError("File not found");
            const extFile = getFileExtension(isFilePath ? source : source.filename).toLowerCase();
            const fileName = isFilePath ? getFileName(source) : source.filename;
            if (isExtensionValid(extFile) == false)
                throw new ZaloApiError(`File extension "${extFile}" is not allowed`);
            const data = {
                filePath: isFilePath ? source : source.filename,
                chunkContent: [],
                params: {},
                source,
            };
            if (isGroupMessage)
                data.params.grid = threadId;
            else
                data.params.toid = threadId;
            switch (extFile) {
                case "jpg":
                case "jpeg":
                case "png":
                case "webp": {
                    const imageData = isFilePath ? await getImageMetaData(ctx, source) : Object.assign(Object.assign({}, source.metadata), { fileName });
                    if (isExceedMaxFileSize(imageData.totalSize))
                        throw new ZaloApiError(`File ${fileName} size exceed maximum size of ${sharefile.max_size_share_file_v3}MB`);
                    data.fileData = imageData;
                    data.fileType = "image";
                    data.params.totalChunk = Math.ceil(data.fileData.totalSize / chunkSize);
                    data.params.fileName = fileName;
                    data.params.clientId = clientId++;
                    data.params.totalSize = imageData.totalSize;
                    data.params.imei = ctx.imei;
                    data.params.isE2EE = 0;
                    data.params.jxl = 0;
                    data.params.chunkId = 1;
                    break;
                }
                case "mp4": {
                    const videoSize = isFilePath ? await getFileSize(source) : source.metadata.totalSize;
                    if (isExceedMaxFileSize(videoSize))
                        throw new ZaloApiError(`File ${fileName} size exceed maximum size of ${sharefile.max_size_share_file_v3}MB`);
                    data.fileType = "video";
                    data.fileData = {
                        fileName,
                        totalSize: videoSize,
                    };
                    data.params.totalChunk = Math.ceil(data.fileData.totalSize / chunkSize);
                    data.params.fileName = fileName;
                    data.params.clientId = clientId++;
                    data.params.totalSize = videoSize;
                    data.params.imei = ctx.imei;
                    data.params.isE2EE = 0;
                    data.params.jxl = 0;
                    data.params.chunkId = 1;
                    break;
                }
                default: {
                    const fileSize = isFilePath ? await getFileSize(source) : source.metadata.totalSize;
                    if (isExceedMaxFileSize(fileSize))
                        throw new ZaloApiError(`File ${fileName} size exceed maximum size of ${sharefile.max_size_share_file_v3}MB`);
                    data.fileType = "others";
                    data.fileData = {
                        fileName,
                        totalSize: fileSize,
                    };
                    data.params.totalChunk = Math.ceil(data.fileData.totalSize / chunkSize);
                    data.params.fileName = fileName;
                    data.params.clientId = clientId++;
                    data.params.totalSize = fileSize;
                    data.params.imei = ctx.imei;
                    data.params.isE2EE = 0;
                    data.params.jxl = 0;
                    data.params.chunkId = 1;
                    break;
                }
            }
            const fileBuffer = isFilePath ? await fs.promises.readFile(source) : source.data;
            for (let i = 0; i < data.params.totalChunk; i++) {
                const formData = new FormData();
                const slicedBuffer = fileBuffer.subarray(i * chunkSize, (i + 1) * chunkSize);
                formData.append("chunkContent", slicedBuffer, {
                    filename: fileName,
                    contentType: "application/octet-stream",
                });
                data.chunkContent[i] = formData;
            }
            attachmentsData.push(data);
        }
        const requests = [], results = [];
        for (let atmIndex = 0; atmIndex < attachmentsData.length; atmIndex++) {
            const data = attachmentsData[atmIndex];
            for (let i = 0; i < data.params.totalChunk; i++) {
                const encryptedParams = utils.encodeAES(JSON.stringify(data.params));
                if (!encryptedParams)
                    throw new ZaloApiError("Failed to encrypt message");
                requests.push(utils
                    .request(utils.makeURL(url + urlType[data.fileType], { type: typeParam, params: encryptedParams }), {
                    method: "POST",
                    headers: data.chunkContent[i].getHeaders(),
                    body: data.chunkContent[i].getBuffer(),
                })
                    .then(async (response) => {
                    /**
                     * @TODO: better type rather than any
                     */
                    const resData = await resolveResponse(ctx, response);
                    if (resData && resData.fileId != "-1" && resData.photoId != "-1")
                        await new Promise((resolve) => {
                            if (data.fileType == "video" || data.fileType == "others") {
                                const uploadCallback = async (wsData) => {
                                    const result = Object.assign(Object.assign(Object.assign({ fileType: data.fileType }, resData), wsData), { totalSize: data.fileData.totalSize, fileName: data.fileData.fileName, checksum: (await getMd5LargeFileObject(data.source, data.fileData.totalSize)).data });
                                    results[atmIndex] = result;
                                    resolve();
                                };
                                ctx.uploadCallbacks.set(resData.fileId.toString(), uploadCallback);
                            }
                            if (data.fileType == "image") {
                                const result = {
                                    fileType: "image",
                                    width: data.fileData.width,
                                    height: data.fileData.height,
                                    totalSize: data.fileData.totalSize,
                                    hdSize: data.fileData.totalSize,
                                    finished: resData.finished,
                                    normalUrl: resData.normalUrl,
                                    hdUrl: resData.hdUrl,
                                    thumbUrl: resData.thumbUrl,
                                    chunkId: resData.chunkId,
                                    photoId: resData.photoId,
                                    clientFileId: resData.clientFileId,
                                };
                                results[atmIndex] = result;
                                resolve();
                            }
                        });
                }));
                data.params.chunkId++;
            }
        }
        await Promise.all(requests);
        return results;
    };
});
