/* eslint-disable unicorn/no-abusive-eslint-disable */
/* eslint-disable */
import { Server } from "@hapi/hapi";
import { Service } from "@hapipal/schmervice";
import { SavedWhatsappBot as Bot } from "@digiresilience/metamigo-db";
import makeWASocket, {
  DisconnectReason,
  proto,
  downloadContentFromMessage,
  MediaType,
  fetchLatestBaileysVersion,
  isJidBroadcast,
  isJidStatusBroadcast,
  useMultiFileAuthState,
} from "@whiskeysockets/baileys";
import fs from "fs";
import workerUtils from "../../worker-utils.js";

export type AuthCompleteCallback = (error?: string) => void;

export default class WhatsappService extends Service {
  connections: { [key: string]: any; } = {};
  loginConnections: { [key: string]: any; } = {};

  static browserDescription: [string, string, string] = [
    "Metamigo",
    "Chrome",
    "2.0",
  ];

  constructor(server: Server, options: never) {
    super(server, options);
  }

  getAuthDirectory(bot: Bot): string {
    return `/baileys/${bot.id}`;
  }

  async initialize(): Promise<void> {
    this.updateConnections();
  }

  async teardown(): Promise<void> {
    this.resetConnections();
  }

  private async sleep(ms: number): Promise<void> {
    console.log(`pausing ${ms}`);
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  private async resetConnections() {
    for (const connection of Object.values(this.connections)) {
      try {
        connection.end(null);
      } catch (error) {
        console.log(error);
      }
    }
    this.connections = {};
  }

  private async createConnection(
    bot: Bot,
    server: Server,
    options: any,
    authCompleteCallback?: any
  ) {
    const directory = this.getAuthDirectory(bot);
    const { state, saveCreds } = await useMultiFileAuthState(directory);
    const msgRetryCounterMap: any = {};
    const socket = makeWASocket({
      ...options,
      auth: state,
      msgRetryCounterMap,
      shouldIgnoreJid: (jid) =>
        isJidBroadcast(jid) || isJidStatusBroadcast(jid),
    });
    let pause = 5000;

    socket.ev.process(async (events) => {
      if (events["connection.update"]) {
        const update = events["connection.update"];
        const {
          connection: connectionState,
          lastDisconnect,
          qr,
          isNewLogin,
        } = update;
        if (qr) {
          console.log("got qr code");
          await this.server.db().whatsappBots.updateQR(bot, qr);
        } else if (isNewLogin) {
          console.log("got new login");
          await this.server.db().whatsappBots.updateVerified(bot, true);
        } else if (connectionState === "open") {
          console.log("opened connection");
        } else if (connectionState === "close") {
          console.log("connection closed due to ", lastDisconnect.error);
          const disconnectStatusCode = (lastDisconnect?.error as any)?.output
            ?.statusCode;

          if (disconnectStatusCode === DisconnectReason.restartRequired) {
            console.log("reconnecting after got new login");
            const updatedBot = await this.findById(bot.id);
            await this.createConnection(updatedBot, server, options);
            authCompleteCallback?.();
          } else if (disconnectStatusCode !== DisconnectReason.loggedOut) {
            console.log("reconnecting");
            await this.sleep(pause);
            pause *= 2;
            this.createConnection(bot, server, options);
          }
        }
      }

      if (events["creds.update"]) {
        console.log("creds update");
        await saveCreds();
      }

      if (events["messages.upsert"]) {
        console.log("messages upsert");
        const upsert = events["messages.upsert"];
        const { messages } = upsert;
        if (messages) {
          await this.queueUnreadMessages(bot, messages);
        }
      }
    });

    this.connections[bot.id] = { socket, msgRetryCounterMap };
  }

  private async updateConnections() {
    this.resetConnections();

    const bots = await this.server.db().whatsappBots.findAll();
    for await (const bot of bots) {
      if (bot.isVerified) {
        const { version, isLatest } = await fetchLatestBaileysVersion();
        console.log(`using WA v${version.join(".")}, isLatest: ${isLatest}`);

        await this.createConnection(bot, this.server, {
          browser: WhatsappService.browserDescription,
          printQRInTerminal: false,
          version,
        });
      }
    }
  }

  private async queueMessage(bot: Bot, webMessageInfo: proto.IWebMessageInfo) {
    const {
      key: { id, fromMe, remoteJid },
      message,
      messageTimestamp,
    } = webMessageInfo;
    if (!fromMe && message && remoteJid !== "status@broadcast") {
      const { audioMessage, documentMessage, imageMessage, videoMessage } =
        message;
      const isMediaMessage =
        audioMessage || documentMessage || imageMessage || videoMessage;

      const messageContent = Object.values(message)[0];
      let messageType: MediaType;
      let attachment: string;
      let filename: string;
      let mimetype: string;
      if (isMediaMessage) {
        if (audioMessage) {
          messageType = "audio";
          filename = id + "." + audioMessage.mimetype.split("/").pop();
          mimetype = audioMessage.mimetype;
        } else if (documentMessage) {
          messageType = "document";
          filename = documentMessage.fileName;
          mimetype = documentMessage.mimetype;
        } else if (imageMessage) {
          messageType = "image";
          filename = id + "." + imageMessage.mimetype.split("/").pop();
          mimetype = imageMessage.mimetype;
        } else if (videoMessage) {
          messageType = "video";
          filename = id + "." + videoMessage.mimetype.split("/").pop();
          mimetype = videoMessage.mimetype;
        }

        const stream = await downloadContentFromMessage(
          messageContent,
          messageType
        );
        let buffer = Buffer.from([]);
        for await (const chunk of stream) {
          buffer = Buffer.concat([buffer, chunk]);
        }
        attachment = buffer.toString("base64");
      }

      if (messageContent || attachment) {
        const receivedMessage = {
          waMessageId: id,
          waMessage: JSON.stringify(webMessageInfo),
          waTimestamp: new Date((messageTimestamp as number) * 1000),
          attachment,
          filename,
          mimetype,
          whatsappBotId: bot.id,
          botPhoneNumber: bot.phoneNumber,
        };

        workerUtils.addJob("whatsapp-message", receivedMessage, {
          jobKey: id,
        });
      }
    }
  }

  private async queueUnreadMessages(
    bot: Bot,
    messages: proto.IWebMessageInfo[]
  ) {
    for await (const message of messages) {
      await this.queueMessage(bot, message);
    }
  }

  async create(
    phoneNumber: string,
    description: string,
    email: string
  ): Promise<Bot> {
    const db = this.server.db();
    const user = await db.users.findBy({ email });
    const row = await db.whatsappBots.insert({
      phoneNumber,
      description,
      userId: user.id,
    });
    return row;
  }

  async unverify(bot: Bot): Promise<Bot> {
    const directory = this.getAuthDirectory(bot);
    fs.rmSync(directory, { recursive: true, force: true });
    return this.server.db().whatsappBots.updateVerified(bot, false);
  }

  async remove(bot: Bot): Promise<number> {
    const directory = this.getAuthDirectory(bot);
    fs.rmSync(directory, { recursive: true, force: true });
    return this.server.db().whatsappBots.remove(bot);
  }

  async findAll(): Promise<Bot[]> {
    return this.server.db().whatsappBots.findAll();
  }

  async findById(id: string): Promise<Bot> {
    return this.server.db().whatsappBots.findById({ id });
  }

  async findByToken(token: string): Promise<Bot> {
    return this.server.db().whatsappBots.findBy({ token });
  }

  async register(bot: Bot, callback: AuthCompleteCallback): Promise<void> {
    const { version } = await fetchLatestBaileysVersion();
    await this.createConnection(bot, this.server, { version }, callback);
  }

  async send(bot: Bot, phoneNumber: string, message: string): Promise<void> {
    const connection = this.connections[bot.id]?.socket;
    const recipient = `${phoneNumber.replace(/\D+/g, "")}@s.whatsapp.net`;
    await connection.sendMessage(recipient, { text: message });
  }

  async receiveSince(bot: Bot, lastReceivedDate: Date): Promise<void> {
    const connection = this.connections[bot.id]?.socket;
    const messages = await connection.messagesReceivedAfter(
      lastReceivedDate,
      false
    );
    for (const message of messages) {
      this.queueMessage(bot, message);
    }
  }

  async receive(
    bot: Bot,
    _lastReceivedDate: Date
  ): Promise<proto.IWebMessageInfo[]> {
    const connection = this.connections[bot.id]?.socket;
    const messages = await connection.loadAllUnreadMessages();
    return messages;
  }
}
