/**
 * @module scenarioParser
 * @description
 * This module exports a function that parses an event and triggers notification scenarios based on the event data.
 * It integrates with event parsing utilities, GraphQL queries, and notification queues to manage the dispatching
 * of notifications via email and app channels. The function processes event data, identifies relevant scenarios,
 * queries user notification preferences, and dispatches notifications accordingly.
 */

const eventParser = require("./eventParser");
const { client: gqlClient } = require("../graphql-client/graphql-client");
const queries = require("../graphql-client/queries");
const { isEmpty, isFunction } = require("lodash");
const { getMatchingScenarios } = require("./scenarioMapper");
const { dispatchEmailsToQueue } = require("./queues/emailQueue");
const { dispatchAppsToQueue } = require("./queues/appQueue");
const { dispatchFcmsToQueue } = require("./queues/fcmQueue"); // NEW

// If true, the user who commits the action will NOT receive notifications; if false, they will.
const FILTER_SELF_FROM_WATCHERS = process.env?.FILTER_SELF_FROM_WATCHERS !== "false";

/**
 * Parses an event and determines matching scenarios for notifications.
 * Queries job watchers and notification settings before triggering scenario builders.
 *
 * @param {Object} req - The request object containing event data, trigger, table, and logger.
 * @param {string} jobIdField - The field path (e.g., "req.body.event.new.id") to extract the job ID.
 * @returns {Promise<void>} Resolves when the parsing and notification dispatching process is complete.
 * @throws {Error} If required request fields (event data, trigger, or table) or body shop data are missing.
 */
const scenarioParser = async (req, jobIdField) => {
  const { event, trigger, table } = req.body;
  const {
    logger,
    sessionUtils: { getBodyshopFromRedis }
  } = req;

  // Step 1: Validate we know what user committed the action that fired the parser

  const hasuraUserRole = event?.session_variables?.["x-hasura-role"];
  const hasuraUserId = event?.session_variables?.["x-hasura-user-id"];

  // Bail if we don't know who started the scenario
  if (hasuraUserRole === "user" && !hasuraUserId) {
    logger.log("No Hasura user ID found, skipping notification parsing", "info", "notifications");
    return;
  }

  // Validate that required fields are present in the request body
  if (!event?.data || !trigger || !table) {
    throw new Error("Missing required request fields: event data, trigger, or table.");
  }

  // Step 2: Extract just the jobId using the provided jobIdField

  let jobId = null;
  if (jobIdField) {
    let keyName = jobIdField;
    const prefix = "req.body.event.new.";
    if (keyName.startsWith(prefix)) {
      keyName = keyName.slice(prefix.length);
    }
    jobId = event.data.new[keyName] || (event.data.old && event.data.old[keyName]) || null;
  }

  if (!jobId) {
    if (process?.env?.NODE_ENV === "development") {
      logger.log(`No jobId found using path "${jobIdField}", skipping notification parsing`, "info", "notifications");
    }
    return;
  }

  // Step 3: Query job watchers associated with the job ID using GraphQL

  const watcherData = await gqlClient.request(queries.GET_JOB_WATCHERS, {
    jobid: jobId
  });

  // Transform watcher data into a simplified format with email and employee details
  let jobWatchers = watcherData?.job_watchers?.map((watcher) => ({
    email: watcher.user_email,
    firstName: watcher?.user?.employee?.first_name,
    lastName: watcher?.user?.employee?.last_name,
    employeeId: watcher?.user?.employee?.id,
    authId: watcher?.user?.authid
  }));

  if (FILTER_SELF_FROM_WATCHERS && hasuraUserRole === "user") {
    jobWatchers = jobWatchers.filter((watcher) => watcher.authId !== hasuraUserId);
  }

  // Exit early if no job watchers are found for this job
  if (isEmpty(jobWatchers)) {
    if (process?.env?.NODE_ENV === "development") {
      logger.log(`No watchers found for jobId "${jobId}", skipping notification parsing`, "info", "notifications");
    }
    return;
  }

  // Step 5: Perform the full event diff now that we know there are watchers

  const eventData = await eventParser({
    newData: event.data.new,
    oldData: event.data.old,
    trigger,
    table,
    jobId
  });

  // Step 6: Extract body shop information from the job data

  const bodyShopId = watcherData?.job?.bodyshop?.id;
  const bodyShopName = watcherData?.job?.bodyshop?.shopname;
  const bodyShopTimezone = watcherData?.job?.bodyshop?.timezone;

  const jobRoNumber = watcherData?.job?.ro_number;
  const jobClaimNumber = watcherData?.job?.clm_no;

  // Validate that body shop data exists, as it’s required for notifications
  if (!bodyShopId || !bodyShopName) {
    throw new Error("No bodyshop data found for this job.");
  }

  // Step 7: Identify scenarios that match the event data and job context

  const matchingScenarios = await getMatchingScenarios(
    {
      ...eventData,
      jobWatchers,
      bodyShopId,
      bodyShopName
    },
    getBodyshopFromRedis
  );

  // Exit early if no matching scenarios are identified
  if (isEmpty(matchingScenarios)) {
    if (process?.env?.NODE_ENV === "development") {
      logger.log(
        `No matching scenarios found for jobId "${jobId}", skipping notification dispatch`,
        "info",
        "notifications"
      );
    }
    return;
  }

  // Combine event data with additional context for scenario processing
  const finalScenarioData = {
    ...eventData,
    jobWatchers,
    bodyShopId,
    bodyShopName,
    bodyShopTimezone,
    matchingScenarios
  };

  // Step 8: Query notification settings for the job watchers

  const associationsData = await gqlClient.request(queries.GET_NOTIFICATION_ASSOCIATIONS, {
    emails: jobWatchers.map((x) => x.email),
    shopid: bodyShopId
  });

  // Exit early if no notification associations are found
  if (isEmpty(associationsData?.associations)) {
    if (process?.env?.NODE_ENV === "development") {
      logger.log(
        `No notification associations found for jobId "${jobId}", skipping notification dispatch`,
        "info",
        "notifications"
      );
    }
    return;
  }

  // Step 9: Filter scenario watchers based on their enabled notification methods

  finalScenarioData.matchingScenarios = finalScenarioData.matchingScenarios.map((scenario) => ({
    ...scenario,
    scenarioWatchers: associationsData.associations
      .filter((assoc) => {
        const settings = assoc.notification_settings && assoc.notification_settings[scenario.key];
        // Include only watchers with at least one enabled notification method (app, email, or FCM)
        return settings && (settings.app || settings.email || settings.fcm);
      })
      .map((assoc) => {
        const settings = assoc.notification_settings[scenario.key];
        const watcherEmail = assoc.useremail;
        const matchingWatcher = jobWatchers.find((watcher) => watcher.email === watcherEmail);

        // Build watcher object with notification preferences and personal details
        return {
          user: watcherEmail,
          email: settings.email,
          app: settings.app,
          fcm: settings.fcm,
          firstName: matchingWatcher?.firstName,
          lastName: matchingWatcher?.lastName,
          employeeId: matchingWatcher?.employeeId,
          associationId: assoc.id
        };
      })
  }));

  // Exit early if no scenarios have eligible watchers after filtering
  if (isEmpty(finalScenarioData?.matchingScenarios)) {
    if (process?.env?.NODE_ENV === "development") {
      logger.log(
        `No eligible watchers after filtering for jobId "${jobId}", skipping notification dispatch`,
        "info",
        "notifications"
      );
    }
    return;
  }

  // Step 10: Build and collect scenarios to dispatch notifications for

  const scenariosToDispatch = [];

  for (const scenario of finalScenarioData.matchingScenarios) {
    // Skip if no watchers or no builder function is defined for the scenario
    if (isEmpty(scenario.scenarioWatchers) || !isFunction(scenario.builder)) {
      continue;
    }

    let eligibleWatchers = scenario.scenarioWatchers;

    // Filter watchers to only those assigned to changed fields, if specified
    if (scenario.matchToUserFields && scenario.matchToUserFields.length > 0) {
      eligibleWatchers = scenario.scenarioWatchers.filter((watcher) =>
        scenario.matchToUserFields.some(
          (field) => eventData.changedFieldNames.includes(field) && eventData.data[field]?.includes(watcher.employeeId)
        )
      );
    }

    // Skip if no watchers remain after filtering
    if (isEmpty(eligibleWatchers)) {
      continue;
    }

    // Step 11: Filter scenario fields to include only those that changed

    const filteredScenarioFields =
      scenario.fields?.filter((field) => eventData.changedFieldNames.includes(field)) || [];

    // Use the scenario’s builder to construct the notification data
    scenariosToDispatch.push(
      scenario.builder({
        trigger: finalScenarioData.trigger.name,
        bodyShopId: finalScenarioData.bodyShopId,
        bodyShopName: finalScenarioData.bodyShopName,
        bodyShopTimezone: finalScenarioData.bodyShopTimezone,
        scenarioKey: scenario.key,
        scenarioTable: scenario.table,
        scenarioFields: filteredScenarioFields,
        scenarioBuilder: scenario.builder,
        scenarioWatchers: eligibleWatchers,
        jobId: finalScenarioData.jobId,
        jobRoNumber: jobRoNumber,
        jobClaimNumber: jobClaimNumber,
        isNew: finalScenarioData.isNew,
        changedFieldNames: finalScenarioData.changedFieldNames,
        changedFields: finalScenarioData.changedFields,
        data: finalScenarioData.data
      })
    );
  }

  if (isEmpty(scenariosToDispatch)) {
    if (process?.env?.NODE_ENV === "development") {
      logger.log(`No scenarios to dispatch for jobId "${jobId}" after building`, "info", "notifications");
    }
    return;
  }

  // Step 12: Dispatch email notifications to the email queue

  const emailsToDispatch = scenariosToDispatch.map((scenario) => scenario?.email);
  if (!isEmpty(emailsToDispatch)) {
    dispatchEmailsToQueue({ emailsToDispatch, logger }).catch((e) =>
      logger.log("Something went wrong dispatching emails to the Email Notification Queue", "error", "queue", null, {
        message: e?.message,
        stack: e?.stack
      })
    );
  }

  // Step 13: Dispatch app notifications to the app queue

  const appsToDispatch = scenariosToDispatch.map((scenario) => scenario?.app);
  if (!isEmpty(appsToDispatch)) {
    dispatchAppsToQueue({ appsToDispatch, logger }).catch((e) =>
      logger.log("Something went wrong dispatching apps to the App Notification Queue", "error", "queue", null, {
        message: e?.message,
        stack: e?.stack
      })
    );
  }

  const fcmsToDispatch = scenariosToDispatch.map((scenario) => scenario?.fcm);
  if (!isEmpty(fcmsToDispatch)) {
    dispatchFcmsToQueue({ fcmsToDispatch, logger }).catch((e) =>
      logger.log("Something went wrong dispatching FCMs to the FCM Notification Queue", "error", "queue", null, {
        message: e?.message,
        stack: e?.stack
      })
    );
  }
};

module.exports = scenarioParser;
