import { randomUUID } from "node:crypto";
import { readdir } from "node:fs/promises";
import QRCode from "qrcode";
import pino from "pino";
import makeWASocket, { DisconnectReason, downloadMediaMessage, getContentType, jidNormalizedUser, } from "@whiskeysockets/baileys";
import { CallbackClient } from "./callback.js";
import { createEncryptedAuthState, EncryptedFileStore } from "./encrypted-store.js";
const maximumInboundMediaBytes = 10_485_760;
function digits(value) {
    const normalized = value?.split("@")[0]?.split(":")[0]?.replace(/\D/g, "");
    return normalized || undefined;
}
function mediaMetadata(value) {
    if (!value)
        return {};
    const result = {};
    for (const key of ["mimetype", "fileLength", "fileName", "caption", "seconds", "ptt", "jpegThumbnail"]) {
        if (value[key] !== undefined && key !== "jpegThumbnail")
            result[key] = value[key];
    }
    return result;
}
function inboundMessage(message) {
    if (!message.message || message.key.fromMe || !message.key.id)
        return undefined;
    const remoteJid = message.key.remoteJid;
    if (!remoteJid || remoteJid === "status@broadcast" || remoteJid.endsWith("@g.us"))
        return undefined;
    const content = message.message.ephemeralMessage?.message ?? message.message.viewOnceMessage?.message ?? message.message;
    const rawType = getContentType(content);
    if (!rawType)
        return undefined;
    let messageType = "system";
    let text;
    let normalizedContent = {};
    if (rawType === "conversation") {
        messageType = "text";
        text = content.conversation ?? undefined;
    }
    else if (rawType === "extendedTextMessage") {
        messageType = "text";
        text = content.extendedTextMessage?.text ?? undefined;
    }
    else if (rawType === "imageMessage") {
        messageType = "image";
        text = content.imageMessage?.caption ?? undefined;
        normalizedContent = mediaMetadata(content.imageMessage);
    }
    else if (rawType === "audioMessage") {
        messageType = "audio";
        normalizedContent = mediaMetadata(content.audioMessage);
    }
    else if (rawType === "videoMessage") {
        messageType = "video";
        text = content.videoMessage?.caption ?? undefined;
        normalizedContent = mediaMetadata(content.videoMessage);
    }
    else if (rawType === "documentMessage") {
        messageType = "document";
        text = content.documentMessage?.caption ?? undefined;
        normalizedContent = mediaMetadata(content.documentMessage);
    }
    else if (rawType === "locationMessage") {
        messageType = "location";
        normalizedContent = { latitude: content.locationMessage?.degreesLatitude, longitude: content.locationMessage?.degreesLongitude, name: content.locationMessage?.name };
    }
    if (messageType === "system")
        return undefined;
    return {
        messageId: message.key.id,
        from: digits(remoteJid),
        timestamp: Number(message.messageTimestamp ?? Math.floor(Date.now() / 1000)),
        messageType,
        text,
        content: normalizedContent,
        customerName: message.pushName || "WhatsApp contact",
    };
}
function mediaFileName(data) {
    const content = data.content && typeof data.content === "object" && !Array.isArray(data.content)
        ? data.content
        : {};
    const supplied = typeof content.fileName === "string" ? content.fileName.trim().replace(/[\\/\u0000-\u001f\u007f]/g, "_") : "";
    if (supplied && supplied.length <= 255)
        return supplied;
    const type = String(data.messageType ?? "media");
    const mime = typeof content.mimetype === "string" ? content.mimetype : "";
    const extension = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("webp") ? "webp"
        : mime.includes("ogg") ? "ogg" : mime.includes("mpeg") ? "mp3" : mime.includes("mp4") ? "mp4"
            : mime.includes("pdf") ? "pdf" : "bin";
    return `${type}-${Date.now()}.${extension}`;
}
function outboundContent(input) {
    const type = String(input.type ?? "text");
    const text = typeof input.text === "string" ? input.text : undefined;
    const content = input.content && typeof input.content === "object" && !Array.isArray(input.content)
        ? input.content
        : {};
    const link = typeof content.link === "string" ? content.link : undefined;
    if (type === "text")
        return { text: text ?? "" };
    if (type === "image" && link)
        return { image: { url: link }, caption: text };
    if (type === "video" && link)
        return { video: { url: link }, caption: text };
    if (type === "audio" && link)
        return { audio: { url: link }, mimetype: String(content.mimetype ?? "audio/mpeg"), ptt: Boolean(content.ptt) };
    if (type === "document" && link)
        return { document: { url: link }, mimetype: String(content.mimetype ?? "application/octet-stream"), fileName: String(content.fileName ?? "document") };
    if (type === "location")
        return { location: { degreesLatitude: Number(content.latitude), degreesLongitude: Number(content.longitude), name: typeof content.name === "string" ? content.name : undefined } };
    throw new Error(`Message type ${type} is not supported by the WhatsApp Web bridge`);
}
export class WhatsAppWebSessionManager {
    sessions = new Map();
    store;
    callbacks;
    logger;
    constructor(configuration) {
        this.store = new EncryptedFileStore(configuration.directory, configuration.encryptionKey);
        this.callbacks = new CallbackClient(configuration.callbackSecret);
        this.logger = pino({ level: configuration.logLevel, redact: ["qr", "qrDataUrl", "accessToken", "message", "*.message"] });
    }
    async restore() {
        const files = await readdir(this.store.directoryPath()).catch(() => []);
        const metadataFiles = files.filter((file) => file.endsWith(".meta.enc"));
        await Promise.all(metadataFiles.map(async (file) => {
            const sessionId = file.slice(0, -".meta.enc".length);
            const metadata = await this.store.read(`${sessionId}.meta`);
            if (metadata)
                await this.start(metadata, false);
        }));
    }
    async create(sessionId, callbackUrl) {
        const existing = this.sessions.get(sessionId);
        if (existing)
            return this.view(existing);
        const metadata = Object.freeze({ sessionId, callbackUrl, createdAt: new Date().toISOString() });
        await this.store.write(`${sessionId}.meta`, metadata);
        return this.start(metadata, true);
    }
    read(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session)
            throw new Error("WhatsApp Web session was not found");
        return this.view(session);
    }
    async disconnect(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session)
            throw new Error("WhatsApp Web session was not found");
        session.closedByUser = true;
        await session.socket?.logout().catch(() => undefined);
        session.socket?.end(undefined);
        session.socket = undefined;
        session.status = "disconnected";
        session.qrDataUrl = undefined;
        session.qrExpiresAt = undefined;
        session.updatedAt = new Date().toISOString();
        await this.store.remove(`${sessionId}.auth`);
        await this.store.remove(`${sessionId}.meta`);
        await this.notify(session, "connection.status", { status: "disconnected" })
            .catch((error) => this.reportCallbackFailure(session, error));
        return this.view(session);
    }
    async send(sessionId, input) {
        const session = this.sessions.get(sessionId);
        if (!session?.socket || session.status !== "connected")
            throw new Error("WhatsApp Web session is not connected");
        const recipient = digits(typeof input.to === "string" ? input.to : undefined);
        if (!recipient)
            throw new Error("Recipient number is invalid");
        const result = await session.socket.sendMessage(`${recipient}@s.whatsapp.net`, outboundContent(input));
        if (!result?.key.id)
            throw new Error("WhatsApp did not return a message identifier");
        return Object.freeze({ messageId: result.key.id, acceptedAt: new Date().toISOString() });
    }
    async start(metadata, notifyInitial) {
        const session = this.sessions.get(metadata.sessionId) ?? {
            metadata, status: "generating_qr", updatedAt: new Date().toISOString(), reconnectAttempts: 0, closedByUser: false,
        };
        this.sessions.set(metadata.sessionId, session);
        session.closedByUser = false;
        session.status = "generating_qr";
        session.updatedAt = new Date().toISOString();
        const auth = await createEncryptedAuthState(this.store, metadata.sessionId);
        const socket = makeWASocket({
            auth: auth.state,
            logger: this.logger.child({ sessionId: metadata.sessionId }),
            markOnlineOnConnect: false,
            syncFullHistory: false,
            generateHighQualityLinkPreview: false,
        });
        session.socket = socket;
        socket.ev.on("creds.update", () => { void auth.saveCreds().catch((error) => this.fail(session, error)); });
        socket.ev.on("connection.update", (update) => {
            void (async () => {
                if (update.qr) {
                    session.status = "awaiting_scan";
                    session.qrDataUrl = await QRCode.toDataURL(update.qr, { width: 320, margin: 1, errorCorrectionLevel: "M" });
                    session.qrExpiresAt = new Date(Date.now() + 55_000).toISOString();
                    session.updatedAt = new Date().toISOString();
                    await this.notify(session, "connection.status", { status: session.status })
                        .catch((error) => this.reportCallbackFailure(session, error));
                }
                if (update.connection === "open") {
                    session.status = "connected";
                    session.qrDataUrl = undefined;
                    session.qrExpiresAt = undefined;
                    session.displayPhoneNumber = digits(jidNormalizedUser(socket.user?.id ?? ""));
                    session.verifiedName = socket.user?.name || undefined;
                    session.lastError = undefined;
                    session.reconnectAttempts = 0;
                    session.updatedAt = new Date().toISOString();
                    await this.notify(session, "connection.status", { status: "connected", displayPhoneNumber: session.displayPhoneNumber, verifiedName: session.verifiedName })
                        .catch((error) => this.reportCallbackFailure(session, error));
                }
                if (update.connection === "close") {
                    const error = update.lastDisconnect?.error;
                    const code = error?.output?.statusCode;
                    const loggedOut = code === DisconnectReason.loggedOut;
                    session.socket = undefined;
                    session.status = loggedOut || session.closedByUser ? "disconnected" : "generating_qr";
                    session.lastError = loggedOut ? "Session logged out from WhatsApp" : error?.message;
                    session.updatedAt = new Date().toISOString();
                    await this.notify(session, "connection.status", { status: session.status, lastError: session.lastError })
                        .catch((callbackError) => this.reportCallbackFailure(session, callbackError));
                    if (loggedOut)
                        await auth.clear();
                    else if (!session.closedByUser) {
                        session.reconnectAttempts += 1;
                        const delay = Math.min(30_000, 1_000 * 2 ** Math.min(session.reconnectAttempts, 5));
                        setTimeout(() => { void this.start(metadata, false).catch((startError) => this.fail(session, startError)); }, delay);
                    }
                }
            })().catch((error) => this.fail(session, error));
        });
        socket.ev.on("messages.upsert", ({ messages }) => {
            for (const message of messages) {
                const data = inboundMessage(message);
                if (data)
                    void (async () => {
                        if (["image", "audio", "video", "document"].includes(String(data.messageType))) {
                            try {
                                const bytes = await downloadMediaMessage(message, "buffer", {}, {
                                    logger: this.logger,
                                    reuploadRequest: socket.updateMediaMessage,
                                });
                                if (bytes.byteLength <= maximumInboundMediaBytes) {
                                    const content = data.content && typeof data.content === "object" && !Array.isArray(data.content)
                                        ? data.content
                                        : {};
                                    data.media = {
                                        base64: bytes.toString("base64"),
                                        contentType: typeof content.mimetype === "string" ? content.mimetype : "application/octet-stream",
                                        fileName: mediaFileName(data),
                                        sizeBytes: bytes.byteLength,
                                    };
                                }
                                else {
                                    data.content.storageStatus = "too_large";
                                }
                            }
                            catch (error) {
                                data.content.storageStatus = "download_failed";
                                this.logger.warn({
                                    sessionId: session.metadata.sessionId,
                                    messageId: message.key.id,
                                    errorName: error instanceof Error ? error.name : "UnknownError",
                                }, "Inbound WhatsApp media could not be downloaded");
                            }
                        }
                        await this.notify(session, "message.received", data, message.key.id ?? randomUUID());
                    })().catch((error) => this.reportCallbackFailure(session, error));
            }
        });
        if (notifyInitial)
            await this.notify(session, "connection.status", { status: "generating_qr" });
        return this.view(session);
    }
    async notify(session, type, data, eventId) {
        await this.callbacks.deliver(session.metadata.callbackUrl, { sessionId: session.metadata.sessionId, eventId, type, data });
    }
    fail(session, error) {
        session.status = "error";
        session.lastError = error instanceof Error ? error.message.slice(0, 500) : "WhatsApp Web session failed";
        session.updatedAt = new Date().toISOString();
        this.logger.error({ sessionId: session.metadata.sessionId, errorName: error instanceof Error ? error.name : "UnknownError" }, "WhatsApp Web session error");
    }
    reportCallbackFailure(session, error) {
        this.logger.error({
            sessionId: session.metadata.sessionId,
            errorName: error instanceof Error ? error.name : "UnknownError",
        }, "ERP callback failed after retries; WhatsApp session remains connected");
    }
    view(session) {
        return Object.freeze({
            sessionId: session.metadata.sessionId,
            status: session.status,
            qrDataUrl: session.qrDataUrl,
            qrExpiresAt: session.qrExpiresAt,
            displayPhoneNumber: session.displayPhoneNumber,
            verifiedName: session.verifiedName,
            lastError: session.lastError,
            updatedAt: session.updatedAt,
        });
    }
}
