const {
  jobAssignedToMeBuilder,
  billPostedBuilder,
  newNoteAddedBuilder,
  scheduledDatesChangedBuilder,
  tasksUpdatedCreatedBuilder,
  jobStatusChangeBuilder,
  jobsAddedToProductionBuilder,
  alternateTransportChangedBuilder,
  newTimeTicketPostedBuilder,
  intakeDeliveryChecklistCompletedBuilder,
  paymentCollectedCompletedBuilder,
  newMediaAddedReassignedBuilder,
  criticalPartsStatusChangedBuilder,
  supplementImportedBuilder,
  partMarkedBackOrderedBuilder
} = require("./scenarioBuilders");
const logger = require("../utils/logger");
const { isFunction } = require("lodash");

/**
 * An array of notification scenario definitions.
 *
 * Each scenario object can include the following properties:
 * - key {string}: The unique scenario name.
 * - table {string}: The table name to check for changes.
 * - fields {Array<string>}: Fields to check for changes.
 * - matchToUserFields {Array<string>}: Fields used to match scenarios to user data.
 * - onNew {boolean|Array<boolean>}: Indicates whether the scenario should be triggered on new data.
 * - builder {Function}: A function to handle the scenario.
 * - onlyTruthyValues {boolean|Array<string>}: Specifies fields that must have truthy values for the scenario to match.
 * - filterCallback {Function}: Optional callback (sync or async) to further filter the scenario based on event data (returns boolean).
 * - enabled {boolean}: If true, the scenario is active; if false or omitted, the scenario is skipped.
 */
const notificationScenarios = [
  {
    key: "job-assigned-to-me",
    enabled: true,
    table: "jobs",
    fields: ["employee_prep", "employee_body", "employee_csr", "employee_refinish"],
    matchToUserFields: ["employee_prep", "employee_body", "employee_csr", "employee_refinish"],
    builder: jobAssignedToMeBuilder
  },
  {
    key: "bill-posted",
    enabled: true,
    table: "bills",
    builder: billPostedBuilder,
    onNew: true
  },
  {
    key: "new-note-added",
    enabled: true,
    table: "notes",
    builder: newNoteAddedBuilder,
    onNew: true
  },
  {
    key: "schedule-dates-changed",
    enabled: true,
    table: "jobs",
    fields: ["scheduled_in", "scheduled_completion", "scheduled_delivery"],
    builder: scheduledDatesChangedBuilder
  },
  {
    key: "tasks-updated-created",
    enabled: true,
    table: "tasks",
    fields: ["updated_at"],
    // onNew: true,
    builder: tasksUpdatedCreatedBuilder
  },
  {
    key: "job-status-change",
    enabled: true,
    table: "jobs",
    fields: ["status"],
    builder: jobStatusChangeBuilder
  },
  {
    key: "job-added-to-production",
    enabled: true,
    table: "jobs",
    fields: ["inproduction"],
    onlyTruthyValues: ["inproduction"],
    builder: jobsAddedToProductionBuilder
  },
  {
    key: "alternate-transport-changed",
    enabled: true,
    table: "jobs",
    fields: ["alt_transport"],
    builder: alternateTransportChangedBuilder
  },
  {
    key: "new-time-ticket-posted",
    enabled: true,
    table: "timetickets",
    builder: newTimeTicketPostedBuilder
  },
  {
    key: "intake-delivery-checklist-completed",
    enabled: true,
    table: "jobs",
    fields: ["intakechecklist", "deliverchecklist"],
    builder: intakeDeliveryChecklistCompletedBuilder
  },
  {
    key: "payment-collected-completed",
    enabled: true,
    table: "payments",
    onNew: true,
    builder: paymentCollectedCompletedBuilder
  },
  {
    // Only works on a non LMS ENV
    key: "new-media-added-reassigned",
    enabled: true,
    table: "documents",
    fields: ["jobid"],
    builder: newMediaAddedReassignedBuilder
  },
  {
    key: "critical-parts-status-changed",
    enabled: true,
    table: "joblines",
    fields: ["status"],
    onlyTruthyValues: ["status"],
    builder: criticalPartsStatusChangedBuilder,
    filterCallback: ({ eventData }) => !eventData?.data?.critical
  },
  {
    key: "part-marked-back-ordered",
    enabled: true,
    table: "joblines",
    fields: ["status"],
    builder: partMarkedBackOrderedBuilder,
    filterCallback: async ({ eventData, getBodyshopFromRedis }) => {
      try {
        const bodyshop = await getBodyshopFromRedis(eventData.bodyShopId);
        return eventData?.data?.status !== bodyshop?.md_order_statuses?.default_bo;
      } catch (err) {
        logger.log("notifications-error-parts-marked-back-ordered", "error", "notifications", "mapper", {
          message: err?.message,
          stack: err?.stack
        });
        return false;
      }
    }
  },
  // Holding off on this one for now, spans multiple tables
  {
    key: "supplement-imported",
    enabled: false,
    builder: supplementImportedBuilder
  }
];

/**
 * Returns an array of scenarios that match the given event data.
 * Supports asynchronous callbacks for additional filtering.
 *
 * @param {Object} eventData - The parsed event data.
 *   Expected properties:
 *     - table: an object with a `name` property (e.g. { name: "tasks", schema: "public" })
 *     - changedFieldNames: an array of changed field names (e.g. [ "description", "updated_at" ])
 *     - isNew: boolean indicating whether the record is new or updated
 *     - data: the new data object (used to check field values)
 *     - (other properties may be added such as jobWatchers, bodyShopId, etc.)
 * @param {Function} getBodyshopFromRedis - Function to retrieve bodyshop data from Redis.
 * @returns {Promise<Array<Object>>} A promise resolving to an array of matching scenario objects.
 */
const getMatchingScenarios = async (eventData, getBodyshopFromRedis) => {
  const matches = [];
  for (const scenario of notificationScenarios) {
    // Check if the scenario is enabled; skip if not explicitly true
    if (scenario.enabled !== true) {
      continue;
    }

    // If eventData has a table, then only scenarios with a table property that matches should be considered.
    if (eventData.table) {
      if (!scenario.table || eventData.table.name !== scenario.table) {
        continue;
      }
    }

    // Check the onNew flag.
    // Allow onNew to be either a boolean or an array of booleans.
    if (Object.prototype.hasOwnProperty.call(scenario, "onNew")) {
      if (Array.isArray(scenario.onNew)) {
        if (!scenario.onNew.includes(eventData.isNew)) continue;
      } else {
        if (eventData.isNew !== scenario.onNew) continue;
      }
    }

    // If the scenario defines fields, ensure at least one of them is present in changedFieldNames.
    if (scenario.fields && scenario.fields.length > 0) {
      const hasMatchingField = scenario.fields.some((field) => eventData.changedFieldNames.includes(field));
      if (!hasMatchingField) {
        continue;
      }
    }

    // OnlyTruthyValues logic:
    // If onlyTruthyValues is defined, check that the new values of specified fields (or all changed fields if true)
    // are truthy. If an array, only check the listed fields, which must be in scenario.fields.
    if (Object.prototype.hasOwnProperty.call(scenario, "onlyTruthyValues")) {
      let fieldsToCheck;

      if (scenario.onlyTruthyValues === true) {
        // If true, check all fields in the scenario that changed
        fieldsToCheck = scenario.fields.filter((field) => eventData.changedFieldNames.includes(field));
      } else if (Array.isArray(scenario.onlyTruthyValues) && scenario.onlyTruthyValues.length > 0) {
        // If an array, check only the specified fields, ensuring they are in scenario.fields
        fieldsToCheck = scenario.onlyTruthyValues.filter(
          (field) => scenario.fields.includes(field) && eventData.changedFieldNames.includes(field)
        );
        // If no fields in onlyTruthyValues match the scenario’s fields or changed fields, skip this scenario
        if (fieldsToCheck.length === 0) {
          continue;
        }
      } else {
        // Invalid onlyTruthyValues (not true or a non-empty array), skip this scenario
        continue;
      }

      // Ensure all fields to check have truthy new values
      const allTruthy = fieldsToCheck.every((field) => Boolean(eventData.data[field]));
      if (!allTruthy) {
        continue;
      }
    }

    // Execute the callback if defined, supporting both sync and async, and filter based on its return value
    if (isFunction(scenario?.filterCallback)) {
      const shouldFilter = await Promise.resolve(
        scenario.filterCallback({
          eventData,
          getBodyshopFromRedis
        })
      );
      if (shouldFilter) {
        continue;
      }
    }

    matches.push(scenario);
  }
  return matches;
};

module.exports = {
  notificationScenarios,
  getMatchingScenarios
};
