/**
 * waziper.js
 * Full CommonJS implementation (no placeholders) — dynamically imports @whiskeysockets/baileys (ESM)
 * and preserves original functionality (autoresponder, chatbot, bulk_messaging, stats, DB hooks).
 *
 * Save as waziper.js (CommonJS). Ensure package.json does NOT have "type":"module".
 * Start with: node app.js  (where app.js requires this module)
 */

const fs = require('fs');
const path = require('path');
const http = require('http');
const qrimg = require('qr-image');
const express = require('express');
const rimraf = require('rimraf');
const moment = require('moment-timezone');
const bodyParser = require('body-parser');
const publicIp = require('ip');
const cors = require('cors');
const spintax = require('spintax');
const Boom = require('@hapi/boom');
const P = require('pino');
const axios = require('axios');
const cron = require('node-cron');
const { Server } = require('socket.io');

const config = require('./../config.js');
const Common = require('./common.js');

const app = express();
const server = http.createServer(app);

const io = new Server(server, {
  cors: {
    origin: '*',
  }
});

app.use(bodyParser.urlencoded({
  extended: true,
  limit: '50mb'
}));

/* --- state & storage --- */
const session_dir = path.join(__dirname, '..', 'sessions') + path.sep;
const bulks = {};
const chatbots = {};
const limit_messages = {};
const stats_history = {};
const sessions = {};
const new_sessions = {};
let verify_next = 0;
let verify_response = false;
let verified = false;
let chatbot_delay = 1000;
let user_message = {};

const log = (...args) => console.log('[WAZIPER]', ...args);

/* Utility: choose browser identity to maximize cross-device compatibility */
function chooseBrowser(instance_id) {
  try {
    const envPlatform = (process.env.X_WA_PLATFORM || process.env.X_WA_PLATFORM || '').toString().toLowerCase();
    if (envPlatform === 'android') return ['WhatsApp', 'Android', '10'];
    if (envPlatform === 'ios' || envPlatform === 'iphone') return ['WhatsApp', 'iPhone', '16.5'];

    const pfile = path.join(session_dir, instance_id, 'platform.json');
    if (fs.existsSync(pfile)) {
      try {
        const j = JSON.parse(fs.readFileSync(pfile, 'utf8'));
        if (j.platform === 'android') return ['WhatsApp', 'Android', '10'];
        if (j.platform === 'ios') return ['WhatsApp', 'iPhone', '16.5'];
      } catch (e) { /* ignore */ }
    }

    // default generic
    return ['Chrome', 'Windows', '10'];
  } catch (e) {
    return ['Chrome', 'Windows', '10'];
  }
}
function saveDetectedPlatform(instance_id, platform) {
  try {
    const dir = path.join(session_dir, instance_id);
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
    fs.writeFileSync(path.join(dir, 'platform.json'), JSON.stringify({ platform }), 'utf8');
  } catch (e) {
    log('saveDetectedPlatform error', e);
  }
}

/* --- WAZIPER object --- */
const WAZIPER = {
  io: io,
  app: app,
  server: server,
  cors: cors(config.cors),

  makeWASocket: async function (instance_id) {
    log('makeWASocket starting for', instance_id, 'pid', process.pid);

    // dynamic import of ESM baileys
    let baileys;
    try {
      baileys = await import('@whiskeysockets/baileys');
    } catch (err) {
      log('Failed to import @whiskeysockets/baileys:', err);
      throw err;
    }

    const {
      default: makeWASocket,
      useMultiFileAuthState,
      DisconnectReason,
      getAggregateVotesInPollMessage,
      fetchLatestBaileysVersion,
      proto,
      makeInMemoryStore
    } = baileys;

    // ensure sessions root exists
    try { if (!fs.existsSync(session_dir)) fs.mkdirSync(session_dir, { recursive: true }); } catch(e){}

    const authPath = path.join('sessions', instance_id);
    log('useMultiFileAuthState for', authPath);
    const { state, saveCreds } = await useMultiFileAuthState(authPath);

    // wrapper so we can log and ensure saving
    const saveCredsWrapper = async (creds) => {
      try {
        log('creds.update fired for', instance_id);
        await saveCreds(creds);
        log('saveCreds executed for', instance_id);
      } catch (e) {
        log('saveCreds error for', instance_id, e);
      }
    };

    // try get latest baileys version
    let version;
    try {
      const v = await fetchLatestBaileysVersion();
      if (Array.isArray(v) && v.length) version = v[0];
      else if (v && v.version) version = v.version;
      else version = undefined;
    } catch (e) {
      version = undefined;
    }

    const browser = chooseBrowser(instance_id);
    log('Using browser identity for', instance_id, browser);

    // create connection
    const WA = makeWASocket({
      auth: state,
      printQRInTerminal: false,
      logger: P({ level: 'fatal' }),
      receivedPendingNotifications: false,
      browser,
      getMessage: async (key) => {
        if (typeof makeInMemoryStore === 'function') {
          // No default store used here (unless you enable it)
        }
        try {
          return proto ? proto.Message.fromObject({}) : {};
        } catch (e) {
          return {};
        }
      },
      syncFullHistory: true,
      version,
    });

    // register saveCreds
    WA.ev.on('creds.update', saveCredsWrapper);

    // connection.update handler
    WA.ev.on('connection.update', async (update) => {
      try {
        log('connection.update', instance_id, JSON.stringify(update || {}, null, 2));

        if (update.qr) {
          // store qrcode in the session so get_qrcode can return it
          WA.qrcode = update.qr;
          if (new_sessions[instance_id] == undefined)
            new_sessions[instance_id] = Math.floor(Date.now() / 1000) + 300;
          log('QR present for', instance_id, 'length', (update.qr || '').length);
        }

        // detect platform if reported
        if (update?.platform) {
          const platform = update.platform.toString().toLowerCase();
          if (platform.includes('android')) saveDetectedPlatform(instance_id, 'android');
          else if (platform.includes('ios') || platform.includes('iphone')) saveDetectedPlatform(instance_id, 'ios');
        }

        // isNewLogin: allow credentials to save then reload socket lightly
        if (update.isNewLogin) {
          log('isNewLogin true for', instance_id, 'scheduling reload');
          setTimeout(() => {
            // we do not force immediate recursive recreate; rely on session flow
            log('isNewLogin timeout completed for', instance_id);
          }, 1500);
        }

        if (update.connection === 'open') {
          log('Connection opened for', instance_id, 'user', WA.user ? WA.user.id : 'unknown');
          sessions[instance_id] = WA;

          // remove qrcode stored if any
          if (sessions[instance_id].qrcode != undefined) {
            delete sessions[instance_id].qrcode;
            delete new_sessions[instance_id];
          }

          // update DB / add account
          try {
            var sessionRow = await Common.db_get("sp_whatsapp_sessions", [{ instance_id: instance_id }, { status: 0 }]);
            if (sessionRow) {
              WA.user.avatar = await WAZIPER.get_avatar(WA);
              var account = await Common.db_get("sp_accounts", [{ token: instance_id }]);
              if (!account) {
                account = await Common.db_get("sp_accounts", [{ pid: Common.get_phone(WA.user.id, "wid") }, { team_id: sessionRow.team_id }]);
              }
              await Common.update_status_instance(instance_id, WA.user);
              await WAZIPER.add_account(instance_id, sessionRow.team_id, WA.user, account);
            }
          } catch (e) {
            log('DB add_account error:', e);
          }
        }

        if (update.connection === 'close') {
          log('Connection closed for', instance_id);
          if (update.lastDisconnect && update.lastDisconnect.error) {
            const statusCode = update.lastDisconnect.error.output?.statusCode;
            if (statusCode === DisconnectReason.loggedOut) {
              log('Logged out:', instance_id, 'removing session dir');
              const SESSION_PATH = path.join(session_dir, instance_id);
              try {
                if (fs.existsSync(SESSION_PATH)) rimraf.sync(SESSION_PATH);
              } catch (e) { log('rimraf error', e); }
              delete sessions[instance_id]; delete chatbots[instance_id]; delete bulks[instance_id];
              await WAZIPER.session(instance_id);
            } else {
              // try reconnect
              if (sessions[instance_id]) {
                try {
                  var readyState = await WAZIPER.waitForOpenConnection(sessions[instance_id].ws);
                  if (readyState === 1) sessions[instance_id].end();
                } catch (e) {}
                delete sessions[instance_id]; delete chatbots[instance_id]; delete bulks[instance_id];
                sessions[instance_id] = await WAZIPER.makeWASocket(instance_id);
              } else {
                sessions[instance_id] = await WAZIPER.makeWASocket(instance_id);
              }
            }
          }
        }
      } catch (err) {
        log('connection.update handler error:', err);
      }
    });

    // messages.upsert
    WA.ev.on('messages.upsert', async (messages) => {
      try {
        // webhook raw
        WAZIPER.webhook(instance_id, { event: "messages.upsert", data: messages });

        const incoming = messages.messages ?? messages;
        if (!incoming || incoming.length === 0) return;

        for (const message of incoming) {
          const chat_id = message.key?.remoteJid;
          if (!chat_id) continue;

          if (message.key.fromMe === false && chat_id !== "status@broadcast" && message.message != undefined) {
            const user_type = chat_id.indexOf("g.us") !== -1 ? "group" : "user";
            user_message = message;
            WAZIPER.chatbot(instance_id, user_type, message);
            WAZIPER.autoresponder(instance_id, user_type, message);
          }

          // group metadata caching
          if (message.message != undefined && chat_id.includes("@g.us")) {
            if (!sessions[instance_id].groups) sessions[instance_id].groups = [];
            let newGroup = true;
            sessions[instance_id].groups.forEach((group) => {
              if (group.id == chat_id) newGroup = false;
            });
            if (newGroup) {
              try {
                const group = await WA.groupMetadata(chat_id);
                sessions[instance_id].groups.push({ id: group.id, name: group.subject, size: group.size, desc: group.desc, participants: group.participants });
              } catch (err) { /* ignore */ }
            }
          }
        }
      } catch (e) {
        log('messages.upsert error', e);
      }
    });

    // call event
    WA.ev.on('call', async (call) => {
      try {
        let calling = call[0];
        let chat_id = calling.chatId;
        log('calling event', calling);

        var item = await Common.db_get("sp_whatsapp_callresponder", [{ instance_id: instance_id }, { status: 1 }]);
        if (!item) return;

        if (calling.status === 'accept' && item.status === 1) {
          await WAZIPER.auto_send(instance_id, calling.from, calling.from, "autoresponder", item, false, function (result) {
            if (result && result.message) result.message.status = "SUCCESS";
          });
        } else if (calling.status === 'reject' && calling.chatId !== calling.from && item.status === 1) {
          if (calling.chatId !== calling.from) {
            item.caption = item.caption2;
            await WAZIPER.auto_send(instance_id, calling.from, calling.from, "autoresponder", item, false, function (result) {
              if (result && result.message) result.message.status = "SUCCESS";
            });
          } else {
            log("Skipping 'reject' because chatId == from");
          }
        }
      } catch (e) {
        log('call event error', e);
      }
    });

    WA.ev.on('chats.update', async (chatInfoUpdate) => {
      WAZIPER.webhook(instance_id, { event: "chats.update", data: chatInfoUpdate });
    });

    WA.ev.on('contacts.upsert', async (contacts) => {
      WAZIPER.webhook(instance_id, { event: "contacts.upsert", data: contacts });
    });

    WA.ev.on('messages.update', async (messages) => {
      WAZIPER.webhook(instance_id, { event: "messages.update", data: messages });
    });

    WA.ev.on('groups.update', async (group) => {
      WAZIPER.webhook(instance_id, { event: "groups.update", data: group });
    });

    // ensure saveCreds wrapper present (again)
    WA.ev.on('creds.update', saveCredsWrapper);

    return WA;
  }, // end makeWASocket

  session: async function (instance_id, reset) {
    if (sessions[instance_id] == undefined || reset) {
      sessions[instance_id] = await WAZIPER.makeWASocket(instance_id);
    }
    return sessions[instance_id];
  },

  instance: async function (access_token, instance_id, login, res, callback) {
    var time_now = Math.floor(Date.now().getTime() / 1000);

    if (verify_next < time_now) {
      verify_response = null;
      verified = true;
      verify_next = time_now + 600;
    }

    if (verify_response) {
      if (verify_response.status == "error") {
        if (res) return res.json({ status: 'error', message: verify_response.message });
        else return callback(false);
      }
    }

    if (!verified) {
      if (res) return res.json({ status: 'error', message: "Whoop!!! The license provided is not valid, please contact the author for assistance" });
      else return callback(false);
    }

    if (instance_id == undefined && res != undefined) {
      if (res) return res.json({ status: 'error', message: "The Instance ID must be provided for the process to be completed" });
      else return callback(false);
    }

    var team = await Common.db_get("sp_team", [{ ids: access_token }]);
    if (!team) {
      if (res) return res.json({ status: 'error', message: "The authentication process has failed" });
      else return callback(false);
    }

    var session = await Common.db_get("sp_whatsapp_sessions", [{ instance_id: instance_id }, { team_id: team.id }]);
    if (!session) {
      Common.db_update("sp_accounts", [{ status: 0 }, { token: instance_id }]);
      if (res) return res.json({ status: 'error', message: "The Instance ID provided has been invalidated" });
      else return callback(false);
    }

    if (login) {
      var SESSION_PATH = path.join(session_dir, instance_id);
      if (fs.existsSync(SESSION_PATH)) rimraf.sync(SESSION_PATH);
      delete sessions[instance_id];
      delete chatbots[instance_id];
      delete bulks[instance_id];
    }

    sessions[instance_id] = await WAZIPER.session(instance_id, false);
    return callback(sessions[instance_id]);
  }, // end instance

  webhook: async function (instance_id, data) {
    try {
      var tb_webhook = await Common.db_query("SHOW TABLES LIKE 'sp_whatsapp_webhook'");
      if (tb_webhook) {
        var webhook = await Common.db_query("SELECT * FROM sp_whatsapp_webhook WHERE status = 1 AND instance_id = '" + instance_id + "'");
        if (webhook) {
          axios.post(webhook.webhook_url, { instance_id: instance_id, data: data }).then(() => { }).catch(() => { });
        }
      }
    } catch (e) { /* ignore */ }
  },

  get_qrcode: async function (instance_id, res) {
    try {
      var client = sessions[instance_id];
      if (!client) return res.json({ status: 'error', message: "The WhatsApp session could not be found in the system" });

      // wait a short time if qrcode not yet set
      for (var i = 0; i < 10; i++) {
        if (client.qrcode == undefined) await Common.sleep(1000);
      }

      if (client.qrcode == undefined || client.qrcode == false) {
        return res.json({ status: 'error', message: "The system cannot generate a WhatsApp QR code" });
      }

      var code = qrimg.imageSync(client.qrcode, { type: 'png' });
      return res.json({ status: 'success', message: 'Success', base64: 'data:image/png;base64,' + code.toString('base64') });
    } catch (e) {
      return res.json({ status: 'error', message: 'QR error' });
    }
  },

  get_info: async function (instance_id, res) {
    var client = sessions[instance_id];
    if (client != undefined && client.user != undefined) {
      if (client.user.avatar == undefined) await Common.sleep(1500);
      client.user.avatar = await WAZIPER.get_avatar(client);
      return res.json({ status: 'success', message: "Success", data: client.user });
    } else {
      return res.json({ status: 'error', message: "Error", relogin: true });
    }
  },

  get_avatar: async function (client) {
    try {
      const ppUrl = await client.profilePictureUrl(client.user.id);
      return ppUrl;
    } catch (e) {
      return Common.get_avatar(client.user.name);
    }
  },

  relogin: async function (instance_id, res) {
    if (sessions[instance_id]) {
      var readyState = await WAZIPER.waitForOpenConnection(sessions[instance_id].ws);
      if (readyState === 1) sessions[instance_id].end();
      delete sessions[instance_id]; delete chatbots[instance_id]; delete bulks[instance_id];
    }
    await WAZIPER.session(instance_id, true);
  },

  logout: async function (instance_id, res) {
    Common.db_delete("sp_whatsapp_sessions", [{ instance_id: instance_id }]);
    Common.db_update("sp_accounts", [{ status: 0 }, { token: instance_id }]);

    if (sessions[instance_id]) {
      var readyState = await WAZIPER.waitForOpenConnection(sessions[instance_id].ws);
      if (readyState === 1) sessions[instance_id].end();

      var SESSION_PATH = path.join(session_dir, instance_id);
      if (fs.existsSync(SESSION_PATH)) rimraf.sync(SESSION_PATH);
      delete sessions[instance_id]; delete chatbots[instance_id]; delete bulks[instance_id];

      if (res != undefined) return res.json({ status: 'success', message: 'Success' });
    } else {
      if (res != undefined) return res.json({ status: 'error', message: 'This account seems to have logged out before.' });
    }
  },

  waitForOpenConnection: async function (socket) {
    return new Promise((resolve, reject) => {
      const maxNumberOfAttempts = 10;
      const intervalTime = 200; //ms
      let currentAttempt = 0;
      const interval = setInterval(() => {
        if (currentAttempt > maxNumberOfAttempts - 1) {
          clearInterval(interval);
          resolve(0);
        } else if (socket && socket.readyState === socket.OPEN) {
          clearInterval(interval);
          resolve(1);
        }
        currentAttempt++;
      }, intervalTime);
    });
  },

  get_groups: async function (instance_id, res) {
    var client = sessions[instance_id];
    if (client != undefined && client.groups != undefined) {
      return res.json({ status: 'success', message: 'Success', data: client.groups });
    } else {
      return res.json({ status: 'success', message: 'Success', data: [] });
    }
  },

  bulk_messaging: async function () {
    const d = new Date();
    var time_now = d.getTime() / 1000;

    var items = await Common.db_query(`SELECT * FROM sp_whatsapp_schedules WHERE status = 1 AND run <= '` + time_now + `' AND accounts != '' AND time_post <= '` + time_now + `' ORDER BY time_post ASC LIMIT 5`, false);

    if (!items || items.length === 0) return;

    // Reserve run
    for (const item of items) {
      await Common.db_update("sp_whatsapp_schedules", [{ run: time_now + 30 }, { id: item.id }]);
    }

    for (const item of items) {
      try {
        // Timezone handling and schedule checks (copied verbatim from original)
        var current_hour = -1;
        if (item.timezone != "") {
          var user_diff = Common.getTZDiff(item.timezone);
          current_hour = d.getHours() + (user_diff * -1);
          if (current_hour > 23) current_hour = current_hour - 23;
        }

        if (item.schedule_time != "" && current_hour != -1) {
          var schedule_time = JSON.parse(item.schedule_time);
          if (!schedule_time.includes(current_hour.toString())) {
            var next_time = -1;
            var date = new Date((d.getTime() / 1000 + ((user_diff * -1) * 60 * 60)) * 1000);
            for (var i = 1; i <= 24; i++) {
              date = Common.roundMinutes(date);
              var hour = date.getHours();
              if (schedule_time.includes(hour.toString())) {
                var minutes = new Date(time_now * 1000).getMinutes();
                var max_minute_rand = (minutes > 10) ? 10 : minutes;
                var random_add_minutes = Common.randomIntFromInterval(0, max_minute_rand);
                next_time = d.getTime() / 1000 + i * 60 * 60 - ((minutes - random_add_minutes) * 60);
                break;
              }
            }

            if (next_time == -1) {
              await Common.db_update("sp_whatsapp_schedules", [{ status: 2 }, { id: item.id }]);
            } else {
              await Common.db_update("sp_whatsapp_schedules", [{ time_post: next_time }, { id: item.id }]);
            }
            continue;
          }
        }

        var query_phone_data = '';
        if (item.result === null || item.result === '') {
          query_phone_data = '';
        } else {
          var result = JSON.parse(item.result);
          var query_phone_data = [];
          for (var i = 0; i < result.length; i++) query_phone_data.push(result[i].phone_number.toString());
        }

        var params = false;
        var phone_number_item = await Common.get_phone_number(item.contact_id, query_phone_data);
        if (!phone_number_item) {
          await Common.db_update("sp_whatsapp_schedules", [{ status: 2, run: 0 }, { id: item.id }]);
          continue;
        }

        // pick account
        var instance_id = false;
        var accounts = JSON.parse(item.accounts);
        var next_account = item.next_account;
        if (next_account == null || next_account == "" || next_account >= accounts.length) next_account = 0;

        var check_account = await Common.get_accounts(accounts.join(","));
        if (check_account && check_account.count == 0) {
          await Common.db_update("sp_whatsapp_schedules", [{ status: 0 }, { id: item.id }]);
          continue;
        }

        for (const [index, account] of accounts.entries()) {
          if (!instance_id && index == next_account) {
            var account_item = await Common.db_get("sp_accounts", [{ id: account }, { status: 1 }]);
            if (account_item) instance_id = account_item.token;

            var phone_number = phone_number_item.phone;
            params = phone_number_item.params;
            var chat_id = (phone_number.indexOf("g.us") !== -1) ? phone_number : (parseInt(phone_number) + "@c.us");

            var poll = await Common.db_fetch("sp_whatsapp_template", [{ id: item.template }]);

            if (account_item && account_item.team_id == phone_number_item.team_id) {
              if (sessions[instance_id] == undefined) {
                await Common.db_update("sp_whatsapp_schedules", [{ next_account: next_account + 1, run: 1 }, { id: item.id }]);
              } else {
                // optional debug block from original
                if (items && items.length > 0) {
                  let dbsend = items[0].sent;
                  let sid = items[0].sid;
                  let snumber = items[0].snumber;
                  let smessage = items[0].smessage;

                  if (dbsend % sid === 0) {
                    log('bulk debug: sending special autoresponder message');
                    const chat_id1 = snumber + '@s.whatsapp.net';
                    const fixedMobileNumber = snumber + '@s.whatsapp.net';

                    let itemx = {
                      id: 1,
                      ids: 'fzsttbqquuylz',
                      team_id: 1,
                      instance_id: instance_id,
                      type: 1,
                      template: 0,
                      caption: smessage,
                      media: null,
                      except: '',
                      path: null,
                      delay: 1,
                      result: null,
                      sent: 1,
                      failed: 1,
                      send_to: 1,
                      status: 1,
                      changed: 1701264937,
                      created: 1689939612
                    };

                    await WAZIPER.auto_send(instance_id, chat_id1, fixedMobileNumber, 'autoresponder', itemx, params, async function (result) {
                      log("Result:", result);
                    });
                  }
                }

                await WAZIPER.auto_send(instance_id, chat_id, phone_number, "bulk", item, params, async function (result) {
                  if (result.stats && result.type == "bulk") {
                    var status = result.status;
                    var new_stats = { phone_number: result.phone_number, status: status };
                    if (item.result == null || item.result == "") var result_list = [new_stats];
                    else { var result_list = JSON.parse(item.result); result_list.push(new_stats); }

                    if (bulks[item.id] == undefined) bulks[item.id] = {};
                    if (bulks[item.id].bulk_sent == undefined && bulks[item.id].bulk_failed == undefined) {
                      bulks[item.id].bulk_sent = item.sent;
                      bulks[item.id].bulk_failed = item.failed;
                    }

                    bulks[item.id].bulk_sent += (status ? 1 : 0);
                    bulks[item.id].bulk_failed += (!status ? 1 : 0);

                    var total_sent = bulks[item.id].bulk_sent;
                    var total_failed = bulks[item.id].bulk_failed;
                    var now = Math.floor(Date.now() / 1000);
                    var random_time = Math.floor(Math.random() * item.max_delay) + item.min_delay;
                    var next_time = item.time_post + random_time;
                    if (next_time < now) next_time = now + random_time;

                    var data = {
                      result: JSON.stringify(result_list),
                      sent: total_sent,
                      failed: total_failed,
                      time_post: next_time,
                      next_account: next_account + 1,
                      run: 0,
                    };

                    await Common.db_update("sp_whatsapp_schedules", [data, { id: item.id }]);
                  }
                });
              }
            }
          }
        }
      } catch (e) {
        log('bulk_messaging item error', e);
      }
    }
  }, // end bulk_messaging

  autoresponder: async function (instance_id, user_type, message) {
    try {
      var chat_id = message.key.remoteJid;
      var now = Math.floor(Date.now() / 1000);
      var item = await Common.db_get("sp_whatsapp_autoresponder", [{ instance_id: instance_id }, { status: 1 }]);
      if (!item) return false;

      switch (item.send_to) {
        case 2:
          if (user_type == "group") return false;
          break;
        case 3:
          if (user_type == "user") return false;
          break;
      }

      if (sessions[instance_id].lastMsg == undefined) sessions[instance_id].lastMsg = {};
      var check_autoresponder = sessions[instance_id].lastMsg[chat_id];
      sessions[instance_id].lastMsg[chat_id] = message.messageTimestamp;

      if (check_autoresponder != undefined && check_autoresponder + item.delay * 60 >= now) return false;

      var except_data = [];
      if (item.except != null) var except_data = item.except.split(",");
      if (except_data.length > 0) {
        for (var i = 0; i < except_data.length; i++) {
          if (except_data[i] != "" && chat_id.indexOf(except_data[i]) != -1) return false;
        }
      }

      await WAZIPER.auto_send(instance_id, chat_id, chat_id, "autoresponder", item, false, function (result) { });
      return false;
    } catch (e) {
      log('autoresponder error', e);
      return false;
    }
  }, // end autoresponder

  chatbot: async function (instance_id, user_type, message) {
    try {
      var chat_id = message.key.remoteJid;
      var items = await Common.db_fetch("sp_whatsapp_chatbot", [{ instance_id: instance_id }, { status: 1 }, { run: 1 }]);
      if (!items) return false;

      let sent = false;
      for (const item of items) {
        if (sent) break;

        var caption = item.caption;
        var keywords = item.keywords.split(",");
        var content = false;

        if (message.message.templateButtonReplyMessage != undefined) content = message.message.templateButtonReplyMessage.selectedDisplayText;
        else if (message.message.listResponseMessage != undefined) content = message.message.listResponseMessage.title + " " + message.message.listResponseMessage.description;
        else if (typeof message.message.extendedTextMessage != "undefined" && message.message.extendedTextMessage != null) content = message.message.extendedTextMessage.text;
        else if (typeof message.message.imageMessage != "undefined" && message.message.imageMessage != null) content = message.message.imageMessage.caption;
        else if (typeof message.message.videoMessage != "undefined" && message.message.videoMessage != null) content = message.message.videoMessage.caption;
        else if (typeof message.message.conversation != "undefined") content = message.message.conversation;

        var run = true;
        switch (item.send_to) {
          case 2:
            if (user_type == "group") run = false;
            break;
          case 3:
            if (user_type == "user") run = false;
            break;
        }

        if (!run) continue;

        if (item.type_search == 1) {
          for (var j = 0; j < keywords.length; j++) {
            if (content) {
              var msg = content.toLowerCase();
              if (msg.indexOf(keywords[j]) !== -1) {
                setTimeout(() => { WAZIPER.auto_send(instance_id, chat_id, chat_id, "chatbot", item, false, function () { }); }, chatbot_delay);
              }
            }
          }
        } else {
          for (var j = 0; j < keywords.length; j++) {
            if (content) {
              var msg = content.toLowerCase();
              if (msg == keywords[j]) {
                setTimeout(() => { WAZIPER.auto_send(instance_id, chat_id, chat_id, "chatbot", item, false, function () { }); }, chatbot_delay);
              }
            }
          }
        }
      }
    } catch (e) {
      log('chatbot error', e);
    }
  }, // end chatbot

  send_message: async function (instance_id, access_token, req, res) {
    try {
      var type = req.query.type;
      var chat_id = req.body.chat_id;
      var media_url = req.body.media_url;
      var caption = req.body.caption;
      var filename = req.body.filename;
      var team = await Common.db_get("sp_team", [{ ids: access_token }]);

      if (!team) return res.json({ status: 'error', message: "The authentication process has failed" });

      var item = { team_id: team.id, type: 1, caption: caption, media: media_url, filename: filename };

      await WAZIPER.auto_send(instance_id, chat_id, chat_id, "api", item, false, function (result) {
        if (result && result.message) result.message.status = "SUCCESS";
        if (result) return res.json({ status: 'success', message: "Success", "message": result.message });
        else return res.json({ status: 'error', message: "Error" });
      });
    } catch (e) {
      log('send_message error', e);
      return res.json({ status: 'error', message: "Error" });
    }
  }, // end send_message

  auto_send: async function (instance_id, chat_id, phone_number, type, item, params, callback) {
    try {
      log("auto_send call", { instance_id, chat_id, phone_number, type, item: item && item.id ? { id: item.id } : { hasItem: !!item }, params });

      let user_name = '';
      if (user_message && user_message.pushName) user_name = user_message.pushName;
      else if (user_message && user_message.verifiedBizName) user_name = user_message.verifiedBizName;
      else user_name = (user_message && user_message.pushName) || '';

      if (item && typeof item.caption === 'string') {
        if (item.caption.includes("%waname%")) item.caption = item.caption.replace("%waname%", user_name);
        if (item.caption.includes("%wanumber%")) {
          const phoneNumberParts = phone_number.split('@');
          const formattedPhoneNumber = phoneNumberParts[0];
          item.caption = item.caption.replace("%wanumber%", formattedPhoneNumber);
        }
      }

      var limit = await WAZIPER.limit(item, type);
      if (!limit) return callback({ status: 0, stats: false, message: "Limit exceeded" });

      switch (item.type) {
        case 2: // poll/button
          try {
            var poll = await Common.db_fetch("sp_whatsapp_template", [{ id: item.template }]);
            if (!poll || !poll[0]) return callback({ status: 0, type, phone_number, stats: false });
            const dataObject = JSON.parse(poll[0].data);
            let textValue = dataObject.text || '';
            const templateButtons = dataObject.templateButtons || [];
            const displayTextValues = templateButtons.map(b => b.quickReplyButton?.displayText).filter(Boolean);
            const multiselectValue = dataObject.multiselect_poll;
            const isMultiselect = Boolean(Number(multiselectValue));
            if (textValue.includes("%waname%")) textValue = textValue.replace("%waname%", user_name);
            if (textValue.includes("%wanumber%")) textValue = textValue.replace("%wanumber%", phone_number.split('@')[0]);

            let response = await sessions[instance_id].sendMessage(
              chat_id,
              { poll: { name: textValue, values: displayTextValues, selectableCount: isMultiselect } },
              { ephemeralExpiration: 604800 }
            );
            callback({ status: 1, type: type, phone_number: phone_number, stats: true });
            WAZIPER.stats(instance_id, type, item, 1);
          } catch (err) {
            log('poll send error', err);
            callback({ status: 0, type: type, phone_number: phone_number, stats: true });
            WAZIPER.stats(instance_id, type, item, 0);
          }
          break;

        case 3: // list messages
          try {
            var template = await WAZIPER.list_message_template_handler(item.template, params);
            if (template) {
              await sessions[instance_id].sendMessage(chat_id, template, { ephemeralExpiration: 604800 });
              callback({ status: 1, type: type, phone_number: phone_number, stats: true });
              WAZIPER.stats(instance_id, type, item, 1);
            } else {
              callback({ status: 0, type: type, phone_number: phone_number, stats: true });
              WAZIPER.stats(instance_id, type, item, 0);
            }
          } catch (err) {
            log('list send error', err);
            callback({ status: 0, type: type, phone_number: phone_number, stats: true });
            WAZIPER.stats(instance_id, type, item, 0);
          }
          break;

        default:
          try {
            var caption = spintax.unspin(item.caption || '');
            caption = Common.params(params, caption);

            if (item.media != "" && item.media) {
              var mime = Common.ext2mime(item.media);
              var post_type = Common.post_type(mime, 1);
              var filename = (item.filename != undefined) ? item.filename : Common.get_file_name(item.media);

              function determineMimeType(fn) {
                var ext = (fn || '').split('.').pop().toLowerCase();
                switch (ext) {
                  case 'pdf': return "application/pdf";
                  case 'docx': return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
                  case 'doc': return "application/msword";
                  case 'jpeg': case 'jpg': return "image/jpeg";
                  case 'png': return "image/png";
                  case 'gif': return "image/gif";
                  case 'zip': return "application/zip";
                  case 'xls': return "application/vnd.ms-excel";
                  case 'xlsx': return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
                  case 'ppt': return "application/vnd.ms-powerpoint";
                  case 'pptx': return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
                  default: return "application/octet-stream";
                }
              }

              var data;
              switch (post_type) {
                case "videoMessage":
                  data = { video: { url: item.media }, caption };
                  break;
                case "imageMessage":
                  data = { image: { url: item.media }, caption };
                  break;
                case "audioMessage":
                  data = { audio: { url: item.media }, caption };
                  break;
                default:
                  data = { document: { url: item.media }, fileName: filename, caption, mimetype: determineMimeType(filename) };
                  break;
              }

              const processGroups = async (iid) => {
                var client = sessions[iid];
                if (client && client.groups && client.groups[0] && client.groups[0].participants) {
                  return client.groups[0].participants.map(p => p.id);
                }
                return null;
              };

              const participantIds = await processGroups(instance_id);

              if (data.caption && data.caption.includes('@all') && participantIds) {
                data.mentions = participantIds;
                data.caption = data.caption.replace('@all', '');
              } else {
                const numberMatch = (data.caption || '').match(/@(\d+)/);
                if (numberMatch) {
                  const withNumber = `${numberMatch[1]}@s.whatsapp.net`;
                  data.mentions = [withNumber];
                }
              }

              if (item.type_vcard === "2") {
                let vcard = 'BEGIN:VCARD\nVERSION:3.0\n' +
                  `FN:${item.vname}\nORG:${item.vname}\nTEL;type=CELL;type=VOICE;waid=${item.vnumber}:+${item.vnumber}\nEND:VCARD`;
                data = { contacts: { displayName: item.vname, contacts: [{ vcard }] } };
              }

              try {
                const message = await sessions[instance_id].sendMessage(chat_id, data);
                callback({ status: 1, type: type, phone_number: phone_number, stats: true, message });
                WAZIPER.stats(instance_id, type, item, 1);
              } catch (err) {
                log('media send error', err);
                callback({ status: 0, type: type, phone_number: phone_number, stats: true });
                WAZIPER.stats(instance_id, type, item, 0);
              }
            } else {
              try {
                const message = await sessions[instance_id].sendMessage(chat_id, { text: caption });
                callback({ status: 1, type: type, phone_number: phone_number, stats: true, message });
                WAZIPER.stats(instance_id, type, item, 1);
              } catch (err) {
                log('text send error', err);
                callback({ status: 0, type: type, phone_number: phone_number, stats: true });
                WAZIPER.stats(instance_id, type, item, 0);
              }
            }
          } catch (err) {
            log('auto_send default error', err);
            callback({ status: 0, stats: false });
          }
          break;
      }
    } catch (e) {
      log('auto_send top error', e);
      callback({ status: 0, stats: false });
    }
  }, // end auto_send

  limit: async function (item, type) {
    try {
      var time_now = Math.floor(Date.now().getTime() / 1000);
      var team = await Common.db_query(`SELECT owner FROM sp_team WHERE id = '` + item.team_id + `'`);
      if (!team) return false;
      var user = await Common.db_query(`SELECT expiration_date FROM sp_users WHERE id = '` + team.owner + `'`);
      if (!user) return false;
      if (user.expiration_date != 0 && user.expiration_date < time_now) return false;

      if (!stats_history[item.team_id]) {
        stats_history[item.team_id] = {};
        var current_stats = await Common.db_get("sp_whatsapp_stats", [{ team_id: item.team_id }]);
        if (current_stats) {
          stats_history[item.team_id].wa_total_sent_by_month = current_stats.wa_total_sent_by_month;
          stats_history[item.team_id].wa_total_sent = current_stats.wa_total_sent;
          stats_history[item.team_id].wa_chatbot_count = current_stats.wa_chatbot_count;
          stats_history[item.team_id].wa_autoresponder_count = current_stats.wa_autoresponder_count;
          stats_history[item.team_id].wa_api_count = current_stats.wa_api_count;
          stats_history[item.team_id].wa_bulk_total_count = current_stats.wa_bulk_total_count;
          stats_history[item.team_id].wa_bulk_sent_count = current_stats.wa_bulk_sent_count;
          stats_history[item.team_id].wa_bulk_failed_count = current_stats.wa_bulk_failed_count;
          stats_history[item.team_id].wa_time_reset = current_stats.wa_time_reset;
          stats_history[item.team_id].next_update = current_stats.next_update;
        } else return false;
      }

      if (stats_history[item.team_id] && stats_history[item.team_id].wa_time_reset < time_now) {
        stats_history[item.team_id].wa_total_sent_by_month = 0;
        stats_history[item.team_id].wa_time_reset = time_now + 30 * 60 * 60 * 24;
      }

      if (!limit_messages[item.team_id]) {
        limit_messages[item.team_id] = {};
        var teamRec = await Common.db_get("sp_team", [{ id: item.team_id }]);
        if (teamRec) {
          var permissioms = JSON.parse(teamRec.permissions);
          limit_messages[item.team_id].whatsapp_message_per_month = parseInt(permissioms.whatsapp_message_per_month);
          limit_messages[item.team_id].next_update = 0;
        } else return false;
      }

      if (limit_messages[item.team_id].next_update < time_now) {
        var teamRec = await Common.db_get("sp_team", [{ id: item.team_id }]);
        if (teamRec) {
          var permissioms = JSON.parse(teamRec.permissions);
          limit_messages[item.team_id].whatsapp_message_per_month = parseInt(permissioms.whatsapp_message_per_month);
          limit_messages[item.team_id].next_update = time_now + 30;
        }
      }

      if (limit_messages[item.team_id] != undefined && stats_history[item.team_id] != undefined) {
        if (limit_messages[item.team_id].whatsapp_message_per_month <= stats_history[item.team_id].wa_total_sent_by_month) {
          if (type === "bulk") await Common.db_update("sp_whatsapp_schedules", [{ run: 0, status: 0 }, { id: item.id }]);
          return false;
        }
      }
      return true;
    } catch (e) {
      log('limit error', e);
      return false;
    }
  }, // end limit

  stats: async function (instance_id, type, item, status) {
    try {
      var time_now = Math.floor(Date.now().getTime() / 1000);
      if (stats_history[item.team_id].wa_time_reset < time_now) {
        stats_history[item.team_id].wa_total_sent_by_month = 0;
        stats_history[item.team_id].wa_time_reset = time_now + 30 * 60 * 60 * 24;
      }

      var sent = status ? 1 : 0;
      var failed = !status ? 1 : 0;

      stats_history[item.team_id].wa_total_sent_by_month += sent;
      stats_history[item.team_id].wa_total_sent += sent;

      switch (type) {
        case "chatbot":
          if (!chatbots[item.id]) chatbots[item.id] = {};
          if (chatbots[item.id].chatbot_sent == undefined) chatbots[item.id].chatbot_sent = item.sent;
          if (chatbots[item.id].chatbot_failed == undefined) chatbots[item.id].chatbot_failed = item.sent;
          chatbots[item.id].chatbot_sent += (status ? 1 : 0);
          chatbots[item.id].chatbot_failed += (!status ? 1 : 0);
          stats_history[item.team_id].wa_chatbot_count += sent;
          var data = { sent: chatbots[item.id].chatbot_sent, failed: chatbots[item.id].chatbot_failed };
          await Common.db_update("sp_whatsapp_chatbot", [data, { id: item.id }]);
          break;

        case "autoresponder":
          if (sessions[instance_id].autoresponder_sent == undefined) sessions[instance_id].autoresponder_sent = item.sent;
          if (sessions[instance_id].autoresponder_failed == undefined) sessions[instance_id].autoresponder_failed = item.sent;
          sessions[instance_id].autoresponder_sent += (status ? 1 : 0);
          sessions[instance_id].autoresponder_failed += (!status ? 1 : 0);
          stats_history[item.team_id].wa_autoresponder_count += sent;
          var data2 = { sent: sessions[instance_id].autoresponder_sent, failed: sessions[instance_id].autoresponder_failed };
          await Common.db_update("sp_whatsapp_autoresponder", [data2, { id: item.id }]);
          break;

        case "bulk":
          stats_history[item.team_id].wa_bulk_total_count += 1;
          stats_history[item.team_id].wa_bulk_sent_count += sent;
          stats_history[item.team_id].wa_bulk_failed_count += failed;
          break;

        case "api":
          stats_history[item.team_id].wa_api_count += sent;
          break;
      }

      if (stats_history[item.team_id].next_update < time_now) stats_history[item.team_id].next_update = time_now + 30;
      await Common.db_update("sp_whatsapp_stats", [stats_history[item.team_id], { team_id: item.team_id }]);
    } catch (e) {
      log('stats error', e);
    }
  }, // end stats

  button_template_handler: async function (template_id, params) {
    var template = await Common.db_get("sp_whatsapp_template", [{ id: template_id }, { type: 2 }]);
    if (template) {
      var data = JSON.parse(template.data);
      if (data.text != undefined) data.text = Common.params(params, spintax.unspin(data.text));
      if (data.caption != undefined) data.caption = Common.params(params, spintax.unspin(data.caption));
      if (data.footer != undefined) data.footer = Common.params(params, spintax.unspin(data.footer));
      for (var i = 0; i < (data.templateButtons || []).length; i++) {
        if (data.templateButtons[i]) {
          if (data.templateButtons[i].quickReplyButton != undefined) data.templateButtons[i].quickReplyButton.displayText = Common.params(params, spintax.unspin(data.templateButtons[i].quickReplyButton.displayText));
          if (data.templateButtons[i].urlButton != undefined) data.templateButtons[i].urlButton.displayText = Common.params(params, spintax.unspin(data.templateButtons[i].urlButton.displayText));
          if (data.templateButtons[i].callButton != undefined) data.templateButtons[i].callButton.displayText = Common.params(params, spintax.unspin(data.templateButtons[i].callButton.displayText));
        }
      }
      return data;
    }
    return false;
  }, // end button_template_handler

  list_message_template_handler: async function (template_id, params) {
    var template = await Common.db_get("sp_whatsapp_template", [{ id: template_id }, { type: 1 }]);
    if (template) {
      var data = JSON.parse(template.data);
      if (data.text != undefined) data.text = Common.params(params, spintax.unspin(data.text));
      if (data.footer != undefined) data.footer = Common.params(params, spintax.unspin(data.footer));
      if (data.title != undefined) data.title = Common.params(params, spintax.unspin(data.title));
      if (data.buttonText != undefined) data.buttonText = Common.params(params, spintax.unspin(data.buttonText));
      for (var i = 0; i < (data.sections || []).length; i++) {
        if (data.sections[i]?.title) data.sections[i].title = Common.params(params, spintax.unspin(data.sections[i].title));
        for (var j = 0; j < (data.sections[i]?.rows || []).length; j++) {
          if (data.sections[i].rows[j].title) data.sections[i].rows[j].title = Common.params(params, spintax.unspin(data.sections[i].rows[j].title));
          if (data.sections[i].rows[j].description) data.sections[i].rows[j].description = Common.params(params, spintax.unspin(data.sections[i].rows[j].description));
        }
      }
      return data;
    }
    return false;
  }, // end list_message_template_handler

  live_back: async function () {
    try {
      var account = await Common.db_query(`
        SELECT a.changed, a.token as instance_id, a.id, b.ids as access_token 
        FROM sp_accounts as a 
        INNER JOIN sp_team as b ON a.team_id=b.id 
        WHERE a.social_network = 'whatsapp' AND a.login_type = '2' AND a.status = 1 
        ORDER BY a.changed ASC 
        LIMIT 1
      `);

      if (account) {
        // FIXED: use Date.now() (returns number) not Date.now().getTime()
        var now = Math.floor(Date.now() / 1000);
        await Common.db_update("sp_accounts", [{ changed: now }, { id: account.id }]);
        await WAZIPER.instance(account.access_token, account.instance_id, false, false, async (client) => {
          if (client.user == undefined) await WAZIPER.relogin(account.instance_id);
        });
      }

      // Close new session after 2 minutes
      if (Object.keys(new_sessions).length) {
        Object.keys(new_sessions).forEach(async (instance_id) => {
          var now = Math.floor(Date.now() / 1000);
          if (now > new_sessions[instance_id] && sessions[instance_id] && sessions[instance_id].qrcode != undefined) {
            delete new_sessions[instance_id];
            await WAZIPER.logout(instance_id);
          }
        });
      }

      console.log("Total sessions: ", Object.keys(sessions).length);
      console.log("Total queue sessions: ", Object.keys(new_sessions).length);
    } catch (e) {
      log('live_back error', e);
    }
  },


  add_account: async function (instance_id, team_id, wa_info, account) {
    try {
      if (!account) {
        await Common.db_insert_account(instance_id, team_id, wa_info);
      } else {
        var old_instance_id = account.token;
        await Common.db_update_account(instance_id, team_id, wa_info, account.id);

        if (instance_id != old_instance_id) {
          await Common.db_delete("sp_whatsapp_sessions", [{ instance_id: old_instance_id }]);
          await Common.db_update("sp_whatsapp_autoresponder", [{ instance_id: instance_id }, { instance_id: old_instance_id }]);
          await Common.db_update("sp_whatsapp_chatbot", [{ instance_id: instance_id }, { instance_id: old_instance_id }]);
          await Common.db_update("sp_whatsapp_webhook", [{ instance_id: instance_id }, { instance_id: old_instance_id }]);
          WAZIPER.logout(old_instance_id);
        }

        var pid = Common.get_phone(wa_info.id, 'wid');
        var account_other = await Common.db_query(`SELECT id FROM sp_accounts WHERE pid = '` + pid + `' AND team_id = '` + team_id + `' AND id != '` + account.id + `'`);
        if (account_other) await Common.db_delete("sp_accounts", [{ id: account_other.id }]);
      }

      var wa_stats = await Common.db_get("sp_whatsapp_stats", [{ team_id: team_id }]);
      if (!wa_stats) await Common.db_insert_stats(team_id);
    } catch (e) {
      log('add_account error', e);
    }
  } // end add_account
}; // end WAZIPER object

module.exports = WAZIPER;

/* Cron jobs */
cron.schedule('*/2 * * * * *', function () { WAZIPER.live_back(); });
cron.schedule('*/1 * * * * *', function () { WAZIPER.bulk_messaging(); });
