const { Queue, Worker } = require("bullmq");
const { sendTaskEmail } = require("../../email/sendemail");
const generateEmailTemplate = require("../../email/generateTemplate");
const { InstanceEndpoints } = require("../../utils/instanceMgr");
const { registerCleanupTask } = require("../../utils/cleanupManager");
const getBullMQPrefix = require("../../utils/getBullMQPrefix");
const devDebugLogger = require("../../utils/devDebugLogger");
const moment = require("moment-timezone");

const EMAIL_CONSOLIDATION_DELAY_IN_MINS = (() => {
  const envValue = process.env?.EMAIL_CONSOLIDATION_DELAY_IN_MINS;
  const parsedValue = envValue ? parseInt(envValue, 10) : NaN;
  return isNaN(parsedValue) ? 3 : Math.max(1, parsedValue); // Default to 3, ensure at least 1
})();

// Base time-related constant (in milliseconds) / DO NOT TOUCH
const EMAIL_CONSOLIDATION_DELAY = EMAIL_CONSOLIDATION_DELAY_IN_MINS * 60000; // 1 minute (base timeout)

// Derived time-related constants based on EMAIL_CONSOLIDATION_DELAY / DO NOT TOUCH, these are pegged to EMAIL_CONSOLIDATION_DELAY
const CONSOLIDATION_KEY_EXPIRATION = EMAIL_CONSOLIDATION_DELAY * 1.5; // 1.5 minutes (90s, buffer for consolidation)
const LOCK_EXPIRATION = EMAIL_CONSOLIDATION_DELAY * 0.25; // 15 seconds (quarter of base, for lock duration)
const RATE_LIMITER_DURATION = EMAIL_CONSOLIDATION_DELAY * 0.1; // 6 seconds (tenth of base, for rate limiting)
const NOTIFICATION_EXPIRATION = EMAIL_CONSOLIDATION_DELAY * 1.5; // 1.5 minutes (matches consolidation key expiration)

let emailAddQueue;
let emailConsolidateQueue;
let emailAddWorker;
let emailConsolidateWorker;

const seconds = (ms) => Math.max(1, Math.ceil(ms / 1000));

const escapeHtml = (s = "") =>
  String(s)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");

/**
 * Initializes the email notification queues and workers.
 *
 * @param {Object} options - Configuration options for queue initialization.
 * @param {Object} options.pubClient - Redis client instance for queue communication.
 * @param {Object} options.logger - Logger instance for logging events and debugging.
 * @returns {Queue} The initialized `emailAddQueue` instance for dispatching notifications.
 */
const loadEmailQueue = async ({ pubClient, logger }) => {
  if (!emailAddQueue || !emailConsolidateQueue) {
    const prefix = getBullMQPrefix();
    const devKey = process.env?.NODE_ENV === "production" ? "prod" : "dev";

    devDebugLogger(`Initializing Email Notification Queues with prefix: ${prefix}`);

    // Queue for adding email notifications
    emailAddQueue = new Queue("emailAdd", {
      prefix,
      connection: pubClient,
      defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
    });

    // Queue for consolidating and sending emails
    emailConsolidateQueue = new Queue("emailConsolidate", {
      prefix,
      connection: pubClient,
      defaultJobOptions: { removeOnComplete: true, removeOnFail: true }
    });

    // Worker to process adding notifications
    emailAddWorker = new Worker(
      "emailAdd",
      async (job) => {
        const { jobId, jobRoNumber, bodyShopName, bodyShopTimezone, body, recipients } = job.data;
        devDebugLogger(`Adding email notifications for jobId ${jobId}`);

        const redisKeyPrefix = `email:${devKey}:notifications:${jobId}`;

        for (const recipient of recipients || []) {
          const { user, firstName, lastName } = recipient;
          if (!user) continue;
          const userKey = `${redisKeyPrefix}:${user}`;
          await pubClient.rpush(userKey, body);
          await pubClient.expire(userKey, seconds(NOTIFICATION_EXPIRATION));
          const detailsKey = `email:${devKey}:recipientDetails:${jobId}:${user}`;
          await pubClient.hsetnx(detailsKey, "firstName", firstName || "");
          await pubClient.hsetnx(detailsKey, "lastName", lastName || "");
          const tzValue = bodyShopTimezone || "UTC";
          await pubClient.hsetnx(detailsKey, "bodyShopTimezone", tzValue);
          await pubClient.expire(detailsKey, seconds(NOTIFICATION_EXPIRATION));
          const recipientsSetKey = `email:${devKey}:recipients:${jobId}`;
          await pubClient.sadd(recipientsSetKey, user);
          await pubClient.expire(recipientsSetKey, seconds(NOTIFICATION_EXPIRATION));
          devDebugLogger(`Stored message for ${user} under ${userKey}: ${body}`);
        }

        const consolidateKey = `email:${devKey}:consolidate:${jobId}`;
        const flagSet = await pubClient.setnx(consolidateKey, "pending");
        if (flagSet) {
          await emailConsolidateQueue.add(
            "consolidate-emails",
            { jobId, jobRoNumber, bodyShopName, bodyShopTimezone },
            {
              jobId: `consolidate-${jobId}`,
              delay: EMAIL_CONSOLIDATION_DELAY,
              attempts: 3,
              backoff: LOCK_EXPIRATION
            }
          );
          devDebugLogger(`Scheduled email consolidation for jobId ${jobId}`);
          await pubClient.expire(consolidateKey, seconds(CONSOLIDATION_KEY_EXPIRATION));
        } else {
          devDebugLogger(`Email consolidation already scheduled for jobId ${jobId}`);
        }
      },
      {
        prefix,
        connection: pubClient,
        concurrency: 5
      }
    );

    // Worker to consolidate and send emails
    emailConsolidateWorker = new Worker(
      "emailConsolidate",
      async (job) => {
        const { jobId, jobRoNumber, bodyShopName } = job.data;
        devDebugLogger(`Consolidating emails for jobId ${jobId}`);

        const lockKey = `lock:${devKey}:emailConsolidate:${jobId}`;
        const lockAcquired = await pubClient.set(lockKey, "locked", "NX", "EX", seconds(LOCK_EXPIRATION));
        if (lockAcquired) {
          try {
            const recipientsSet = `email:${devKey}:recipients:${jobId}`;
            const recipients = await pubClient.smembers(recipientsSet);
            for (const recipient of recipients) {
              const userKey = `email:${devKey}:notifications:${jobId}:${recipient}`;
              const detailsKey = `email:${devKey}:recipientDetails:${jobId}:${recipient}`;
              const messages = await pubClient.lrange(userKey, 0, -1);
              if (messages.length > 0) {
                const details = await pubClient.hgetall(detailsKey);
                const firstName = details.firstName || "User";
                const multipleUpdateString = messages.length > 1 ? "Updates" : "Update";
                const subject = `${multipleUpdateString} for job ${jobRoNumber || "N/A"} at ${bodyShopName}`;
                const timezone = moment.tz.zone(details?.bodyShopTimezone) ? details.bodyShopTimezone : "UTC";
                const emailBody = generateEmailTemplate({
                  header: `${multipleUpdateString} for Job ${jobRoNumber || "N/A"}`,
                  subHeader: `Dear ${firstName},`,
                  dateLine: moment().tz(timezone).format("MM/DD/YYYY hh:mm a"),
                  body: `
                        <p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 100%;">There have been updates to job ${jobRoNumber || "N/A"} at ${bodyShopName}:</p>
                      </td></tr></table></th>
                    </tr></tbody></table>
                    <table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
                      <th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
                        <ul style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 1%; padding-left: 30px;">
                          ${messages.map((msg) => `<li style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%;">${escapeHtml(msg)}</li>`).join("")}
                        </ul>
                      </td></tr></table></th>
                    </tr><tbody></table>
                    <table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
                      <th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
                        <p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;"><a href="${InstanceEndpoints()}/manage/jobs/${jobId}" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">Please check the job for more details.</a></p>
                  `
                });
                await sendTaskEmail({
                  to: recipient,
                  subject,
                  type: "html",
                  html: emailBody
                });
                devDebugLogger(
                  `Sent consolidated email to ${recipient} for jobId ${jobId} with ${messages.length} updates`
                );
                await pubClient.del(userKey);
                await pubClient.del(detailsKey);
              }
            }
            await pubClient.del(recipientsSet);
            await pubClient.del(`email:${devKey}:consolidate:${jobId}`);
          } catch (err) {
            logger.log(`email-queue-consolidation-error`, "ERROR", "notifications", "api", {
              message: err?.message,
              stack: err?.stack
            });
            throw err;
          } finally {
            await pubClient.del(lockKey);
          }
        } else {
          devDebugLogger(`Skipped email consolidation for jobId ${jobId} - lock held by another worker`);
        }
      },
      {
        prefix,
        connection: pubClient,
        concurrency: 1,
        limiter: { max: 1, duration: RATE_LIMITER_DURATION }
      }
    );

    // Event handlers for workers
    emailAddWorker.on("completed", (job) => devDebugLogger(`Email add job ${job.id} completed`));
    emailConsolidateWorker.on("completed", (job) => devDebugLogger(`Email consolidate job ${job.id} completed`));

    emailAddWorker.on("failed", (job, err) =>
      logger.log(`add-email-queue-failed`, "ERROR", "notifications", "api", {
        message: err?.message,
        stack: err?.stack
      })
    );
    emailConsolidateWorker.on("failed", (job, err) =>
      logger.log(`email-consolidation-job-failed`, "ERROR", "notifications", "api", {
        message: err?.message,
        stack: err?.stack
      })
    );

    // Register cleanup task instead of direct process listeners
    const shutdown = async () => {
      devDebugLogger("Closing email queue workers...");
      await Promise.all([emailAddWorker.close(), emailConsolidateWorker.close()]);
      devDebugLogger("Email queue workers closed");
    };
    registerCleanupTask(shutdown);
  }

  return emailAddQueue;
};

/**
 * Retrieves the initialized `emailAddQueue` instance.
 *
 * @returns {Queue} The `emailAddQueue` instance for adding notifications.
 * @throws {Error} If `emailAddQueue` is not initialized.
 */
const getQueue = () => {
  if (!emailAddQueue) {
    throw new Error("Email add queue not initialized. Ensure loadEmailQueue is called during bootstrap.");
  }
  return emailAddQueue;
};

/**
 * Dispatches email notifications to the `emailAddQueue` for processing.
 *
 * @param {Object} options - Options for dispatching notifications.
 * @param {Array} options.emailsToDispatch - Array of email notification objects.
 * @param {Object} options.logger - Logger instance for logging dispatch events.
 * @returns {Promise<void>} Resolves when all notifications are added to the queue.
 */
// eslint-disable-next-line no-unused-vars
const dispatchEmailsToQueue = async ({ emailsToDispatch, logger }) => {
  const emailAddQueue = getQueue();

  for (const email of emailsToDispatch) {
    const { jobId, bodyShopName, bodyShopTimezone, body, recipients } = email;
    let { jobRoNumber } = email;

    // Make sure Jobs that have not been coverted yet can still get notifications
    if (jobRoNumber === null) {
      jobRoNumber = "N/A";
    }

    if (!jobId || !jobRoNumber || !bodyShopName || !body || !recipients.length) {
      devDebugLogger(
        `Skipping email dispatch for jobId ${jobId} due to missing data: ` +
          `jobRoNumber=${jobRoNumber || "N/A"}, bodyShopName=${bodyShopName}, body=${body}, recipients=${recipients.length}`
      );
      continue;
    }

    await emailAddQueue.add(
      "add-email-notification",
      { jobId, jobRoNumber, bodyShopName, bodyShopTimezone, body, recipients },
      { jobId: `${jobId}-${Date.now()}` }
    );
    devDebugLogger(`Added email notification to queue for jobId ${jobId} with ${recipients.length} recipients`);
  }
};

module.exports = { loadEmailQueue, getQueue, dispatchEmailsToQueue };
