import { Server } from "@hapi/hapi";
import { Service } from "@hapipal/schmervice";
import makeWASocket, {
  DisconnectReason,
  proto,
  downloadContentFromMessage,
  MediaType,
  fetchLatestBaileysVersion,
  isJidBroadcast,
  isJidStatusBroadcast,
  useMultiFileAuthState,
} from "@whiskeysockets/baileys";
import fs from "fs";

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] = [
    "Bridge",
    "Chrome",
    "2.0",
  ];

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

  getBaseDirectory(): string {
    return `/home/node/baileys`;
  }

  getBotDirectory(id: string): string {
    return `${this.getBaseDirectory()}/${id}`;
  }

  getAuthDirectory(id: string): string {
    return `${this.getBotDirectory(id)}/auth`;
  }

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

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

  private async sleep(ms: number): Promise<void> {
    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(
    botID: string,
    server: Server,
    options: any,
    authCompleteCallback?: any,
  ) {
    const authDirectory = this.getAuthDirectory(botID);
    const { state, saveCreds } = await useMultiFileAuthState(authDirectory);
    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");
          const botDirectory = this.getBotDirectory(botID);
          const qrPath = `${botDirectory}/qr.txt`;
          fs.writeFileSync(qrPath, qr, "utf8");
          const verifiedFile = `${botDirectory}/verified`;
          if (fs.existsSync(verifiedFile)) {
            fs.rmSync(verifiedFile);
          }
        } else if (isNewLogin) {
          console.log("got new login");
          const botDirectory = this.getBotDirectory(botID);
          const verifiedFile = `${botDirectory}/verified`;
          fs.writeFileSync(verifiedFile, "");
        } 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");
            await this.createConnection(botID, server, options);
            authCompleteCallback?.();
          } else if (disconnectStatusCode !== DisconnectReason.loggedOut) {
            console.log("reconnecting");
            await this.sleep(pause);
            pause *= 2;
            this.createConnection(botID, 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(botID, messages);
        }
      }
    });

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

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

    const baseDirectory = this.getBaseDirectory();
    const botIDs = fs.readdirSync(baseDirectory);
    console.log({ botIDs });
    for await (const botID of botIDs) {
      const directory = this.getBotDirectory(botID);
      const verifiedFile = `${directory}/verified`;
      if (fs.existsSync(verifiedFile)) {
        const { version, isLatest } = await fetchLatestBaileysVersion();
        console.log(`using WA v${version.join(".")}, isLatest: ${isLatest}`);

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

  private async queueMessage(
    botID: string,
    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 | null | undefined;
      let mimetype: string | null | undefined;
      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,
          // @ts-ignore
          messageType,
        );
        let buffer = Buffer.from([]);
        for await (const chunk of stream) {
          buffer = Buffer.concat([buffer, chunk]);
        }
        attachment = buffer.toString("base64");
      }

      // @ts-ignore
      if (messageContent || attachment) {
        const receivedMessage = {
          waMessageId: id,
          waMessage: JSON.stringify(webMessageInfo),
          waTimestamp: new Date((messageTimestamp as number) * 1000),
          // @ts-ignore
          attachment,
          filename,
          mimetype,
          whatsappBotId: botID,
        };

        const message =
          webMessageInfo?.message?.conversation ??
          webMessageInfo?.message?.extendedTextMessage?.text ??
          webMessageInfo?.message?.imageMessage?.caption ??
          webMessageInfo?.message?.videoMessage?.caption;

        const payload = {
          message,
          sender: webMessageInfo.key.remoteJid?.split("@")[0],
        };

        await fetch(
          `${process.env.BRIDGE_FRONTEND_URL}/api/whatsapp/bots/${botID}/receive`,
          {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify(payload),
          },
        );
      }
    }
  }

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

  getBot(botID: string): Record<string, any> {
    const botDirectory = this.getBotDirectory(botID);
    const qrPath = `${botDirectory}/qr.txt`;
    const verifiedFile = `${botDirectory}/verified`;
    const qr = fs.existsSync(qrPath) ? fs.readFileSync(qrPath, "utf8") : null;
    const verified = fs.existsSync(verifiedFile);

    return { qr, verified };
  }

  async unverify(botID: string): Promise<void> {
    const botDirectory = this.getBotDirectory(botID);
    fs.rmSync(botDirectory, { recursive: true, force: true });
  }

  async register(
    botID: string,
    callback?: AuthCompleteCallback,
  ): Promise<void> {
    const { version } = await fetchLatestBaileysVersion();
    await this.createConnection(
      botID,
      this.server,
      { version, browser: WhatsappService.browserDescription },
      callback,
    );
    callback?.();
  }

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

  async receive(
    botID: string,
    _lastReceivedDate: Date,
  ): Promise<proto.IWebMessageInfo[]> {
    const connection = this.connections[botID]?.socket;
    console.log({ connection });
    const messages = await connection.loadAllUnreadMessages();

    return messages;
  }
}
