import FormData from "form-data";
import fs from "node:fs/promises";
import { ZaloApiError } from "../Errors/ZaloApiError.js";
import { ThreadType } from "../models/index.js";
import { apiFactory, getClientMessageType, getFileExtension, getFileName, getGifMetaData, getMd5LargeFileObject, removeUndefinedKeys, resolveResponse, } from "../utils.js";
const attachmentUrlType = {
    image: "photo_original/send?",
    gif: "gif?",
    video: "asyncfile/msg?",
    others: "asyncfile/msg?",
};
function prepareQMSGAttach(quote) {
    const quoteData = quote;
    if (typeof quoteData.content == "string")
        return quoteData.propertyExt;
    if (quoteData.msgType == "chat.todo")
        return {
            properties: {
                color: 0,
                size: 0,
                type: 0,
                subType: 0,
                ext: '{"shouldParseLinkOrContact":0}',
            },
        };
    return Object.assign(Object.assign({}, quoteData.content), { thumbUrl: quoteData.content.thumb, oriUrl: quoteData.content.href, normalUrl: quoteData.content.href });
}
function prepareQMSG(quote) {
    const quoteData = quote;
    if (quoteData.msgType == "chat.todo" && typeof quoteData.content == "object" && typeof quoteData.content.params == "string") {
        return JSON.parse(quoteData.content.params).item.content;
    }
    return "";
}
export var TextStyle;
(function (TextStyle) {
    TextStyle["Bold"] = "b";
    TextStyle["Italic"] = "i";
    TextStyle["Underline"] = "u";
    TextStyle["StrikeThrough"] = "s";
    TextStyle["Red"] = "c_db342e";
    TextStyle["Orange"] = "c_f27806";
    TextStyle["Yellow"] = "c_f7b503";
    TextStyle["Green"] = "c_15a85f";
    TextStyle["Small"] = "f_13";
    TextStyle["Big"] = "f_18";
    TextStyle["UnorderedList"] = "lst_1";
    TextStyle["OrderedList"] = "lst_2";
    TextStyle["Indent"] = "ind_$";
})(TextStyle || (TextStyle = {}));
export var Urgency;
(function (Urgency) {
    Urgency[Urgency["Default"] = 0] = "Default";
    Urgency[Urgency["Important"] = 1] = "Important";
    Urgency[Urgency["Urgent"] = 2] = "Urgent";
})(Urgency || (Urgency = {}));
export const sendMessageFactory = apiFactory()((api, ctx, utils) => {
    const serviceURLs = {
        message: {
            [ThreadType.User]: utils.makeURL(`${api.zpwServiceMap.chat[0]}/api/message`, {
                nretry: 0,
            }),
            [ThreadType.Group]: utils.makeURL(`${api.zpwServiceMap.group[0]}/api/group`, {
                nretry: 0,
            }),
        },
        attachment: {
            [ThreadType.User]: `${api.zpwServiceMap.file[0]}/api/message/`,
            [ThreadType.Group]: `${api.zpwServiceMap.file[0]}/api/group/`,
        },
    };
    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 getGroupLayoutId() {
        return Date.now();
    }
    async function send(data) {
        if (!Array.isArray(data))
            data = [data];
        const requests = [];
        for (const each of data) {
            requests.push((async () => {
                const response = await utils.request(each.url, {
                    method: "POST",
                    body: each.body,
                    headers: each.headers,
                });
                return await resolveResponse(ctx, response);
            })());
        }
        return await Promise.all(requests);
    }
    async function upthumb(source, url) {
        const formData = new FormData();
        const buffer = typeof source == "string" ? await fs.readFile(source) : source.data;
        formData.append("fileContent", buffer, {
            filename: "blob",
            contentType: "image/png",
        });
        const params = {
            clientId: Date.now(),
            imei: ctx.imei,
        };
        const encryptedParams = utils.encodeAES(JSON.stringify(params));
        if (!encryptedParams)
            throw new ZaloApiError("Failed to encrypt message");
        const response = await utils.request(utils.makeURL(url + "upthumb?", {
            params: encryptedParams,
        }), {
            method: "POST",
            headers: formData.getHeaders(),
            body: formData.getBuffer(),
        });
        return await resolveResponse(ctx, response);
    }
    function handleMentions(type, msg, mentions) {
        let totalMentionLen = 0;
        const mentionsFinal = Array.isArray(mentions) && type == ThreadType.Group
            ? mentions
                .filter((m) => m.pos >= 0 && m.uid && m.len > 0)
                .map((m) => {
                totalMentionLen += m.len;
                return {
                    pos: m.pos,
                    uid: m.uid,
                    len: m.len,
                    type: m.uid == "-1" ? 1 : 0,
                };
            })
            : [];
        if (totalMentionLen > msg.length) {
            throw new ZaloApiError("Invalid mentions: total mention characters exceed message length");
        }
        return {
            mentionsFinal,
            msgFinal: msg,
        };
    }
    function handleStyles(params, styles) {
        if (styles)
            Object.assign(params, {
                textProperties: JSON.stringify({
                    styles: styles.map((e) => {
                        var _a;
                        const styleFinal = Object.assign(Object.assign({}, e), { indentSize: undefined, st: e.st == TextStyle.Indent
                                ? TextStyle.Indent.replace(/\$/g, `${(_a = e.indentSize) !== null && _a !== void 0 ? _a : 1}0`)
                                : e.st });
                        removeUndefinedKeys(styleFinal);
                        return styleFinal;
                    }),
                    ver: 0,
                }),
            });
    }
    function handleUrgency(params, urgency) {
        if (urgency == Urgency.Important || urgency == Urgency.Urgent) {
            Object.assign(params, { metaData: { urgency } });
        }
    }
    async function handleMessage({ msg, styles, urgency, mentions, quote, ttl }, threadId, type) {
        if (!msg || msg.length == 0)
            throw new ZaloApiError("Missing message content");
        const isGroupMessage = type == ThreadType.Group;
        const { mentionsFinal, msgFinal } = handleMentions(type, msg, mentions);
        msg = msgFinal;
        if (quote) {
            if (typeof quote.content != "string" && quote.msgType == "webchat") {
                throw new ZaloApiError("This kind of `webchat` quote type is not available");
            }
            if (quote.msgType == "group.poll") {
                throw new ZaloApiError("The `group.poll` quote type is not available");
            }
        }
        const isMentionsValid = mentionsFinal.length > 0 && isGroupMessage;
        const params = quote
            ? {
                toid: isGroupMessage ? undefined : threadId,
                grid: isGroupMessage ? threadId : undefined,
                message: msg,
                clientId: Date.now(),
                mentionInfo: isMentionsValid ? JSON.stringify(mentionsFinal) : undefined,
                qmsgOwner: quote.uidFrom,
                qmsgId: quote.msgId,
                qmsgCliId: quote.cliMsgId,
                qmsgType: getClientMessageType(quote.msgType),
                qmsgTs: quote.ts,
                qmsg: typeof quote.content == "string" ? quote.content : prepareQMSG(quote),
                imei: isGroupMessage ? undefined : ctx.imei,
                visibility: isGroupMessage ? 0 : undefined,
                qmsgAttach: isGroupMessage ? JSON.stringify(prepareQMSGAttach(quote)) : undefined,
                qmsgTTL: quote.ttl,
                ttl: ttl !== null && ttl !== void 0 ? ttl : 0,
            }
            : {
                message: msg,
                clientId: Date.now(),
                mentionInfo: isMentionsValid ? JSON.stringify(mentionsFinal) : undefined,
                imei: isGroupMessage ? undefined : ctx.imei,
                ttl: ttl !== null && ttl !== void 0 ? ttl : 0,
                visibility: isGroupMessage ? 0 : undefined,
                toid: isGroupMessage ? undefined : threadId,
                grid: isGroupMessage ? threadId : undefined,
            };
        handleStyles(params, styles);
        handleUrgency(params, urgency);
        removeUndefinedKeys(params);
        const encryptedParams = utils.encodeAES(JSON.stringify(params));
        if (!encryptedParams)
            throw new ZaloApiError("Failed to encrypt message");
        const finalServiceUrl = new URL(serviceURLs.message[type]);
        if (quote) {
            finalServiceUrl.pathname = finalServiceUrl.pathname + "/quote";
        }
        else {
            finalServiceUrl.pathname =
                finalServiceUrl.pathname +
                    "/" +
                    (isGroupMessage ? (params.mentionInfo ? "mention" : "sendmsg") : "sms");
        }
        return {
            url: finalServiceUrl.toString(),
            body: new URLSearchParams({ params: encryptedParams }),
        };
    }
    async function handleAttachment({ msg, attachments, mentions, quote, ttl, urgency }, threadId, type) {
        if (!attachments)
            throw new ZaloApiError("Missing attachments");
        if (!Array.isArray(attachments))
            attachments = [attachments];
        if (attachments.length == 0)
            throw new ZaloApiError("Missing attachments");
        const firstSource = attachments[0];
        const isFilePath = typeof firstSource == "string";
        const firstExtFile = getFileExtension(isFilePath ? firstSource : firstSource.filename);
        const isSingleFile = attachments.length == 1;
        const isGroupMessage = type == ThreadType.Group;
        const canBeDesc = isSingleFile && ["jpg", "jpeg", "png", "webp"].includes(firstExtFile);
        const gifFiles = attachments.filter((e) => getFileExtension(typeof e == "string" ? e : e.filename) == "gif");
        attachments = attachments.filter((e) => getFileExtension(typeof e == "string" ? e : e.filename) != "gif");
        const uploadAttachment = attachments.length == 0 ? [] : await api.uploadAttachment(attachments, threadId, type);
        const attachmentsData = [];
        let indexInGroupLayout = 0;
        const groupLayoutId = getGroupLayoutId().toString();
        const { mentionsFinal, msgFinal } = handleMentions(type, msg, mentions);
        msg = msgFinal;
        const isMentionsValid = mentionsFinal.length > 0 && isGroupMessage && attachments.length == 1;
        const isMultiFile = attachments.length > 1;
        let clientId = Date.now();
        for (const attachment of uploadAttachment) {
            let data;
            switch (attachment.fileType) {
                case "image": {
                    data = {
                        fileType: attachment.fileType,
                        params: {
                            photoId: attachment.photoId,
                            clientId: (clientId++).toString(),
                            desc: msg,
                            width: attachment.width,
                            height: attachment.height,
                            toid: isGroupMessage ? undefined : String(threadId),
                            grid: isGroupMessage ? String(threadId) : undefined,
                            rawUrl: attachment.normalUrl,
                            hdUrl: attachment.hdUrl,
                            thumbUrl: attachment.thumbUrl,
                            oriUrl: isGroupMessage ? attachment.normalUrl : undefined,
                            normalUrl: isGroupMessage ? undefined : attachment.normalUrl,
                            hdSize: String(attachment.totalSize),
                            zsource: -1,
                            ttl: ttl !== null && ttl !== void 0 ? ttl : 0,
                            jcp: '{"convertible":"jxl"}',
                            groupLayoutId: isMultiFile ? groupLayoutId : undefined,
                            isGroupLayout: isMultiFile ? 1 : undefined,
                            idInGroup: isMultiFile ? indexInGroupLayout++ : undefined,
                            totalItemInGroup: isMultiFile ? uploadAttachment.length : undefined,
                            extMsgProp: isMultiFile ? `{"groupMediaMsg":{"groupLayoutId":"${groupLayoutId}"}}` : undefined,
                            mentionInfo: isMentionsValid && canBeDesc && !quote ? JSON.stringify(mentionsFinal) : undefined,
                        },
                        body: new URLSearchParams(),
                    };
                    break;
                }
                case "video": {
                    data = {
                        fileType: attachment.fileType,
                        params: {
                            fileId: attachment.fileId,
                            checksum: attachment.checksum,
                            checksumSha: "",
                            extention: getFileExtension(attachment.fileName),
                            totalSize: attachment.totalSize,
                            fileName: attachment.fileName,
                            clientId: attachment.clientFileId,
                            fType: 1,
                            fileCount: 0,
                            fdata: "{}",
                            toid: isGroupMessage ? undefined : String(threadId),
                            grid: isGroupMessage ? String(threadId) : undefined,
                            fileUrl: attachment.fileUrl,
                            zsource: -1,
                            ttl: ttl !== null && ttl !== void 0 ? ttl : 0,
                        },
                        body: new URLSearchParams(),
                    };
                    break;
                }
                case "others": {
                    data = {
                        fileType: attachment.fileType,
                        params: {
                            fileId: attachment.fileId,
                            checksum: attachment.checksum,
                            checksumSha: "",
                            extention: getFileExtension(attachment.fileName),
                            totalSize: attachment.totalSize,
                            fileName: attachment.fileName,
                            clientId: attachment.clientFileId,
                            fType: 1,
                            fileCount: 0,
                            fdata: "{}",
                            toid: isGroupMessage ? undefined : String(threadId),
                            grid: isGroupMessage ? String(threadId) : undefined,
                            fileUrl: attachment.fileUrl,
                            zsource: -1,
                            ttl: ttl !== null && ttl !== void 0 ? ttl : 0,
                        },
                        body: new URLSearchParams(),
                    };
                    break;
                }
            }
            handleUrgency(data.params, urgency);
            removeUndefinedKeys(data.params);
            const encryptedParams = utils.encodeAES(JSON.stringify(data.params));
            if (!encryptedParams)
                throw new ZaloApiError("Failed to encrypt message");
            data.body.append("params", encryptedParams);
            attachmentsData.push(data);
        }
        for (const gif of gifFiles) {
            const isFilePath = typeof gif == "string";
            const gifData = isFilePath ? await getGifMetaData(ctx, gif) : Object.assign(Object.assign({}, gif.metadata), { fileName: gif.filename });
            if (isExceedMaxFileSize(gifData.totalSize))
                throw new ZaloApiError(`File ${isFilePath ? getFileName(gif) : gif.filename} size exceed maximum size of ${sharefile.max_size_share_file_v3}MB`);
            const _upthumb = await upthumb(gif, serviceURLs.attachment[ThreadType.User]);
            const formData = new FormData();
            formData.append("chunkContent", isFilePath ? await fs.readFile(gif) : gif.data, {
                filename: isFilePath ? getFileName(gif) : gif.filename,
                contentType: "application/octet-stream",
            });
            const params = {
                clientId: Date.now().toString(),
                fileName: gifData.fileName,
                totalSize: gifData.totalSize,
                width: gifData.width,
                height: gifData.height,
                msg: msg,
                type: 1,
                ttl: ttl !== null && ttl !== void 0 ? ttl : 0,
                visibility: isGroupMessage ? 0 : undefined,
                toid: isGroupMessage ? undefined : threadId,
                grid: isGroupMessage ? threadId : undefined,
                thumb: _upthumb.url,
                checksum: (await getMd5LargeFileObject(gif, gifData.totalSize)).data,
                totalChunk: 1,
                chunkId: 1,
            };
            handleUrgency(params, urgency);
            removeUndefinedKeys(params);
            const encryptedParams = utils.encodeAES(JSON.stringify(params));
            if (!encryptedParams)
                throw new ZaloApiError("Failed to encrypt message");
            attachmentsData.push({
                query: {
                    params: encryptedParams,
                    type: "1",
                },
                body: formData.getBuffer(),
                headers: formData.getHeaders(),
                fileType: "gif",
            });
        }
        const responses = [];
        for (const data of attachmentsData) {
            responses.push({
                url: utils.makeURL(serviceURLs.attachment[type] + attachmentUrlType[data.fileType], Object.assign({
                    nretry: "0",
                }, data.query || {})),
                body: data.body,
                headers: data.fileType == "gif" ? data.headers : {},
            });
        }
        return responses;
    }
    /**
     * Send a message to a thread
     *
     * @param message Message content
     * @param threadId group or user id
     * @param type Message type (User or Group)
     * @param quote Message or GroupMessage instance (optional), used for quoting
     *
     * @throws {ZaloApiError | ZaloApiMissingImageMetadataGetter}
     */
    return async function sendMessage(message, threadId, type = ThreadType.User) {
        if (!message)
            throw new ZaloApiError("Missing message content");
        if (!threadId)
            throw new ZaloApiError("Missing threadId");
        if (typeof message == "string")
            message = { msg: message };
        let { msg, attachments, mentions } = message;
        const { quote, ttl, styles, urgency } = message;
        if (attachments && !Array.isArray(attachments)) {
            attachments = [attachments];
        }
        if (!msg && (!attachments || (attachments && attachments.length == 0)))
            throw new ZaloApiError("Missing message content");
        if (attachments && isExceedMaxFile(attachments.length))
            throw new ZaloApiError("Exceed maximum file of " + sharefile.max_file);
        const responses = {
            message: null,
            attachment: [],
        };
        if (attachments && attachments.length > 0) {
            const firstExtFile = getFileExtension(typeof attachments[0] == "string" ? attachments[0] : attachments[0].filename);
            const isSingleFile = attachments.length == 1;
            const canBeDesc = isSingleFile && ["jpg", "jpeg", "png", "webp"].includes(firstExtFile);
            if ((!canBeDesc && msg.length > 0) || (msg.length > 0 && quote)) {
                // send message and attachment separately
                await handleMessage(message, threadId, type).then(async (data) => {
                    responses.message = (await send(data))[0];
                });
                msg = "";
                mentions = undefined;
            }
            const handledData = await handleAttachment({ msg, mentions, attachments, quote, ttl, styles, urgency }, threadId, type);
            responses.attachment = await send(handledData);
            msg = "";
        }
        if (msg.length > 0) {
            const handledData = await handleMessage(message, threadId, type);
            responses.message = (await send(handledData))[0];
        }
        return responses;
    };
});
