// src/services/expiryService.server.ts
/**
 * @file Expiry Date Tracking Service
 * Handles inventory management, expiry date calculations, and expiry alerts.
 * Provides functionality for tracking food items and notifying users about expiring items.
 */
import type { Logger } from 'pino';
import { expiryRepo, receiptRepo } from './db/index.db';
import type {
  StorageLocation,
  AlertMethod,
  UserInventoryItem,
  AddInventoryItemRequest,
  UpdateInventoryItemRequest,
  ExpiryDateRange,
  AddExpiryRangeRequest,
  ExpiryAlertSettings,
  UpdateExpiryAlertSettingsRequest,
  ExpiringItemsResponse,
  InventoryQueryOptions,
  ExpiryRangeQueryOptions,
  CalculateExpiryOptions,
  ExpiryAlertType,
} from '../types/expiry';

/**
 * Default expiry warning threshold in days
 */
const DEFAULT_EXPIRY_WARNING_DAYS = 7;

/**
 * Number of days to consider an item "expiring soon"
 */
const EXPIRING_SOON_THRESHOLD = 7;

/**
 * Number of days to consider for "this month" expiry grouping
 */
const THIS_MONTH_THRESHOLD = 30;

// ============================================================================
// INVENTORY MANAGEMENT
// ============================================================================

/**
 * Adds an item to the user's inventory.
 * If no expiry date is provided, attempts to calculate one based on storage location.
 * @param userId The user's ID
 * @param item The item to add
 * @param logger Pino logger instance
 * @returns The created inventory item with computed expiry status
 */
export const addInventoryItem = async (
  userId: string,
  item: AddInventoryItemRequest,
  logger: Logger,
): Promise<UserInventoryItem> => {
  const itemLogger = logger.child({ userId, itemName: item.item_name });
  itemLogger.info('Adding item to inventory');

  // If no expiry date provided and we have purchase date + location, try to calculate
  if (!item.expiry_date && item.purchase_date && item.location) {
    const calculatedExpiry = await calculateExpiryDate(
      {
        master_item_id: item.master_item_id,
        item_name: item.item_name,
        storage_location: item.location,
        purchase_date: item.purchase_date,
      },
      itemLogger,
    );

    if (calculatedExpiry) {
      itemLogger.debug({ calculatedExpiry }, 'Calculated expiry date from storage location');
      item.expiry_date = calculatedExpiry;
    }
  }

  const inventoryItem = await expiryRepo.addInventoryItem(userId, item, itemLogger);
  itemLogger.info({ inventoryId: inventoryItem.inventory_id }, 'Item added to inventory');

  return inventoryItem;
};

/**
 * Updates an existing inventory item.
 * @param inventoryId The inventory item ID
 * @param userId The user's ID (for authorization)
 * @param updates The updates to apply
 * @param logger Pino logger instance
 * @returns The updated inventory item
 */
export const updateInventoryItem = async (
  inventoryId: number,
  userId: string,
  updates: UpdateInventoryItemRequest,
  logger: Logger,
): Promise<UserInventoryItem> => {
  logger.debug({ inventoryId, userId, updates }, 'Updating inventory item');
  return expiryRepo.updateInventoryItem(inventoryId, userId, updates, logger);
};

/**
 * Marks an inventory item as consumed.
 * @param inventoryId The inventory item ID
 * @param userId The user's ID (for authorization)
 * @param logger Pino logger instance
 */
export const markItemConsumed = async (
  inventoryId: number,
  userId: string,
  logger: Logger,
): Promise<void> => {
  logger.debug({ inventoryId, userId }, 'Marking item as consumed');
  await expiryRepo.markAsConsumed(inventoryId, userId, logger);
  logger.info({ inventoryId }, 'Item marked as consumed');
};

/**
 * Deletes an inventory item.
 * @param inventoryId The inventory item ID
 * @param userId The user's ID (for authorization)
 * @param logger Pino logger instance
 */
export const deleteInventoryItem = async (
  inventoryId: number,
  userId: string,
  logger: Logger,
): Promise<void> => {
  logger.debug({ inventoryId, userId }, 'Deleting inventory item');
  await expiryRepo.deleteInventoryItem(inventoryId, userId, logger);
  logger.info({ inventoryId }, 'Item deleted from inventory');
};

/**
 * Gets a single inventory item by ID.
 * @param inventoryId The inventory item ID
 * @param userId The user's ID (for authorization)
 * @param logger Pino logger instance
 * @returns The inventory item
 */
export const getInventoryItemById = async (
  inventoryId: number,
  userId: string,
  logger: Logger,
): Promise<UserInventoryItem> => {
  return expiryRepo.getInventoryItemById(inventoryId, userId, logger);
};

/**
 * Gets the user's inventory with optional filtering and pagination.
 * @param options Query options
 * @param logger Pino logger instance
 * @returns Paginated inventory items
 */
export const getInventory = async (
  options: InventoryQueryOptions,
  logger: Logger,
): Promise<{ items: UserInventoryItem[]; total: number }> => {
  logger.debug({ userId: options.user_id }, 'Fetching user inventory');
  return expiryRepo.getInventory(options, logger);
};

// ============================================================================
// EXPIRING ITEMS
// ============================================================================

/**
 * Gets items grouped by expiry urgency for dashboard display.
 * @param userId The user's ID
 * @param logger Pino logger instance
 * @returns Items grouped by expiry status with counts
 */
export const getExpiringItemsGrouped = async (
  userId: string,
  logger: Logger,
): Promise<ExpiringItemsResponse> => {
  logger.debug({ userId }, 'Fetching expiring items grouped by urgency');

  // Get all expiring items within 30 days + expired items
  const expiringThisMonth = await expiryRepo.getExpiringItems(userId, THIS_MONTH_THRESHOLD, logger);
  const expiredItems = await expiryRepo.getExpiredItems(userId, logger);

  // Group items by urgency
  const today = new Date();
  today.setHours(0, 0, 0, 0);

  const expiringToday: UserInventoryItem[] = [];
  const expiringThisWeek: UserInventoryItem[] = [];
  const expiringLater: UserInventoryItem[] = [];

  for (const item of expiringThisMonth) {
    if (item.days_until_expiry === null) {
      continue;
    }

    if (item.days_until_expiry === 0) {
      expiringToday.push(item);
    } else if (item.days_until_expiry <= EXPIRING_SOON_THRESHOLD) {
      expiringThisWeek.push(item);
    } else {
      expiringLater.push(item);
    }
  }

  const response: ExpiringItemsResponse = {
    expiring_today: expiringToday,
    expiring_this_week: expiringThisWeek,
    expiring_this_month: expiringLater,
    already_expired: expiredItems,
    counts: {
      today: expiringToday.length,
      this_week: expiringThisWeek.length,
      this_month: expiringLater.length,
      expired: expiredItems.length,
      total:
        expiringToday.length + expiringThisWeek.length + expiringLater.length + expiredItems.length,
    },
  };

  logger.info(
    {
      userId,
      counts: response.counts,
    },
    'Expiring items fetched',
  );

  return response;
};

/**
 * Gets items expiring within a specified number of days.
 * @param userId The user's ID
 * @param daysAhead Number of days to look ahead
 * @param logger Pino logger instance
 * @returns Items expiring within the specified timeframe
 */
export const getExpiringItems = async (
  userId: string,
  daysAhead: number,
  logger: Logger,
): Promise<UserInventoryItem[]> => {
  logger.debug({ userId, daysAhead }, 'Fetching expiring items');
  return expiryRepo.getExpiringItems(userId, daysAhead, logger);
};

/**
 * Gets items that have already expired.
 * @param userId The user's ID
 * @param logger Pino logger instance
 * @returns Expired items
 */
export const getExpiredItems = async (
  userId: string,
  logger: Logger,
): Promise<UserInventoryItem[]> => {
  logger.debug({ userId }, 'Fetching expired items');
  return expiryRepo.getExpiredItems(userId, logger);
};

// ============================================================================
// EXPIRY DATE CALCULATION
// ============================================================================

/**
 * Calculates an estimated expiry date based on item and storage location.
 * Uses expiry_date_ranges table for reference data.
 * @param options Calculation options
 * @param logger Pino logger instance
 * @returns Calculated expiry date string (ISO format) or null if unable to calculate
 */
export const calculateExpiryDate = async (
  options: CalculateExpiryOptions,
  logger: Logger,
): Promise<string | null> => {
  const { master_item_id, category_id, item_name, storage_location, purchase_date } = options;

  logger.debug(
    {
      masterItemId: master_item_id,
      categoryId: category_id,
      itemName: item_name,
      storageLocation: storage_location,
    },
    'Calculating expiry date',
  );

  // Look up expiry range for this item/category/pattern
  const expiryRange = await expiryRepo.getExpiryRangeForItem(storage_location, logger, {
    masterItemId: master_item_id,
    categoryId: category_id,
    itemName: item_name,
  });

  if (!expiryRange) {
    logger.debug('No expiry range found for item');
    return null;
  }

  // Calculate expiry date using typical_days
  const purchaseDateTime = new Date(purchase_date);
  purchaseDateTime.setDate(purchaseDateTime.getDate() + expiryRange.typical_days);

  const expiryDateStr = purchaseDateTime.toISOString().split('T')[0];

  logger.debug(
    {
      purchaseDate: purchase_date,
      typicalDays: expiryRange.typical_days,
      expiryDate: expiryDateStr,
    },
    'Expiry date calculated',
  );

  return expiryDateStr;
};

/**
 * Gets expiry date ranges with optional filtering.
 * @param options Query options
 * @param logger Pino logger instance
 * @returns Paginated expiry date ranges
 */
export const getExpiryRanges = async (
  options: ExpiryRangeQueryOptions,
  logger: Logger,
): Promise<{ ranges: ExpiryDateRange[]; total: number }> => {
  return expiryRepo.getExpiryRanges(options, logger);
};

/**
 * Adds a new expiry date range (admin operation).
 * @param range The range to add
 * @param logger Pino logger instance
 * @returns The created expiry range
 */
export const addExpiryRange = async (
  range: AddExpiryRangeRequest,
  logger: Logger,
): Promise<ExpiryDateRange> => {
  logger.info(
    { storageLocation: range.storage_location, typicalDays: range.typical_days },
    'Adding expiry range',
  );
  return expiryRepo.addExpiryRange(range, logger);
};

// ============================================================================
// EXPIRY ALERTS
// ============================================================================

/**
 * Gets the user's expiry alert settings.
 * @param userId The user's ID
 * @param logger Pino logger instance
 * @returns Array of alert settings
 */
export const getAlertSettings = async (
  userId: string,
  logger: Logger,
): Promise<ExpiryAlertSettings[]> => {
  return expiryRepo.getUserAlertSettings(userId, logger);
};

/**
 * Updates the user's expiry alert settings for a specific alert method.
 * @param userId The user's ID
 * @param alertMethod The alert delivery method
 * @param settings The settings to update
 * @param logger Pino logger instance
 * @returns Updated alert settings
 */
export const updateAlertSettings = async (
  userId: string,
  alertMethod: AlertMethod,
  settings: UpdateExpiryAlertSettingsRequest,
  logger: Logger,
): Promise<ExpiryAlertSettings> => {
  logger.debug({ userId, alertMethod, settings }, 'Updating alert settings');
  return expiryRepo.upsertAlertSettings(userId, alertMethod, settings, logger);
};

/**
 * Processes expiry alerts for all users.
 * This should be called by a scheduled worker job.
 * @param logger Pino logger instance
 * @returns Number of alerts sent
 */
export const processExpiryAlerts = async (logger: Logger): Promise<number> => {
  logger.info('Starting expiry alert processing');

  // Get all users with expiring items who have alerts enabled
  const usersToNotify = await expiryRepo.getUsersWithExpiringItems(logger);
  logger.debug({ userCount: usersToNotify.length }, 'Found users to notify');

  let alertsSent = 0;

  for (const user of usersToNotify) {
    try {
      // Get the expiring items for this user
      const expiringItems = await expiryRepo.getExpiringItems(
        user.user_id,
        user.days_before_expiry,
        logger,
      );

      if (expiringItems.length === 0) {
        continue;
      }

      // Send notification based on alert method
      switch (user.alert_method) {
        case 'email':
          await sendExpiryEmailAlert(user.user_id, user.email, expiringItems, logger);
          break;
        case 'push':
          // TODO: Implement push notifications
          logger.debug({ userId: user.user_id }, 'Push notifications not yet implemented');
          break;
        case 'in_app':
          // TODO: Implement in-app notifications
          logger.debug({ userId: user.user_id }, 'In-app notifications not yet implemented');
          break;
      }

      // Log the alert and mark as sent
      for (const item of expiringItems) {
        await expiryRepo.logAlert(
          user.user_id,
          'expiring_soon',
          user.alert_method,
          item.item_name,
          logger,
          {
            pantryItemId: item.inventory_id,
            expiryDate: item.expiry_date,
            daysUntilExpiry: item.days_until_expiry,
          },
        );
      }

      await expiryRepo.markAlertSent(user.user_id, user.alert_method, logger);
      alertsSent++;
    } catch (error) {
      const err = error instanceof Error ? error : new Error(String(error));
      logger.error({ err, userId: user.user_id }, 'Error processing expiry alert for user');
    }
  }

  logger.info({ alertsSent }, 'Expiry alert processing completed');
  return alertsSent;
};

/**
 * Sends an email alert about expiring items.
 * @param userId The user's ID
 * @param email The user's email
 * @param items The expiring items
 * @param logger Pino logger instance
 */
const sendExpiryEmailAlert = async (
  userId: string,
  email: string,
  items: UserInventoryItem[],
  logger: Logger,
): Promise<void> => {
  const alertLogger = logger.child({ userId, email, itemCount: items.length });
  alertLogger.info('Sending expiry alert email');

  // Group items by urgency
  const expiredItems = items.filter((i) => i.days_until_expiry !== null && i.days_until_expiry < 0);
  const todayItems = items.filter((i) => i.days_until_expiry === 0);
  const soonItems = items.filter(
    (i) => i.days_until_expiry !== null && i.days_until_expiry > 0 && i.days_until_expiry <= 3,
  );
  const laterItems = items.filter((i) => i.days_until_expiry !== null && i.days_until_expiry > 3);

  // Build the email content
  const subject =
    todayItems.length > 0 || expiredItems.length > 0
      ? '⚠️ Food Items Expiring Today or Already Expired!'
      : `🕐 ${items.length} Food Item${items.length > 1 ? 's' : ''} Expiring Soon`;

  const buildItemList = (itemList: UserInventoryItem[], emoji: string): string => {
    if (itemList.length === 0) return '';
    return itemList
      .map((item) => {
        const daysText =
          item.days_until_expiry === 0
            ? 'today'
            : item.days_until_expiry === 1
              ? 'tomorrow'
              : item.days_until_expiry !== null && item.days_until_expiry < 0
                ? `${Math.abs(item.days_until_expiry)} day${Math.abs(item.days_until_expiry) > 1 ? 's' : ''} ago`
                : `in ${item.days_until_expiry} days`;
        const location = item.location ? ` (${item.location})` : '';
        return `${emoji} <strong>${item.item_name}</strong>${location} - expires ${daysText}`;
      })
      .join('<br>');
  };

  let htmlBody = '';

  if (expiredItems.length > 0) {
    htmlBody += `<h3 style="color: #dc3545;">Already Expired (${expiredItems.length})</h3>
      <p>${buildItemList(expiredItems, '❌')}</p>`;
  }

  if (todayItems.length > 0) {
    htmlBody += `<h3 style="color: #fd7e14;">Expiring Today (${todayItems.length})</h3>
      <p>${buildItemList(todayItems, '⚠️')}</p>`;
  }

  if (soonItems.length > 0) {
    htmlBody += `<h3 style="color: #ffc107;">Expiring Within 3 Days (${soonItems.length})</h3>
      <p>${buildItemList(soonItems, '🕐')}</p>`;
  }

  if (laterItems.length > 0) {
    htmlBody += `<h3 style="color: #28a745;">Expiring This Week (${laterItems.length})</h3>
      <p>${buildItemList(laterItems, '📅')}</p>`;
  }

  const html = `
    <div style="font-family: sans-serif; padding: 20px; max-width: 600px;">
      <h2 style="color: #333;">Food Expiry Alert</h2>
      <p>The following items in your pantry need attention:</p>
      ${htmlBody}
      <hr style="margin: 20px 0; border: none; border-top: 1px solid #eee;">
      <p style="color: #666; font-size: 14px;">
        Visit your <a href="${process.env.FRONTEND_URL || 'https://flyer-crawler.projectium.com'}/inventory">inventory page</a>
        to manage these items. You can also find
        <a href="${process.env.FRONTEND_URL || 'https://flyer-crawler.projectium.com'}/recipes/suggestions">recipe suggestions</a>
        to use them before they expire!
      </p>
      <p style="color: #999; font-size: 12px;">
        To manage your alert preferences, visit your <a href="${process.env.FRONTEND_URL || 'https://flyer-crawler.projectium.com'}/settings">settings page</a>.
      </p>
    </div>
  `;

  // Build plain text version
  const buildTextList = (itemList: UserInventoryItem[]): string => {
    return itemList
      .map((item) => {
        const daysText =
          item.days_until_expiry === 0
            ? 'today'
            : item.days_until_expiry === 1
              ? 'tomorrow'
              : item.days_until_expiry !== null && item.days_until_expiry < 0
                ? `${Math.abs(item.days_until_expiry)} day(s) ago`
                : `in ${item.days_until_expiry} days`;
        return `  - ${item.item_name} - expires ${daysText}`;
      })
      .join('\n');
  };

  let textBody = 'Food Expiry Alert\n\nThe following items need attention:\n\n';
  if (expiredItems.length > 0) {
    textBody += `Already Expired:\n${buildTextList(expiredItems)}\n\n`;
  }
  if (todayItems.length > 0) {
    textBody += `Expiring Today:\n${buildTextList(todayItems)}\n\n`;
  }
  if (soonItems.length > 0) {
    textBody += `Expiring Within 3 Days:\n${buildTextList(soonItems)}\n\n`;
  }
  if (laterItems.length > 0) {
    textBody += `Expiring This Week:\n${buildTextList(laterItems)}\n\n`;
  }
  textBody += 'Visit your inventory page to manage these items.\n\nFlyer Crawler';

  try {
    await emailService.sendEmail(
      {
        to: email,
        subject,
        text: textBody,
        html,
      },
      alertLogger,
    );
    alertLogger.info('Expiry alert email sent successfully');
  } catch (error) {
    alertLogger.error({ err: error }, 'Failed to send expiry alert email');
    throw error;
  }
};

// ============================================================================
// RECEIPT INTEGRATION
// ============================================================================

/**
 * Adds items from a confirmed receipt to the user's inventory.
 * @param userId The user's ID
 * @param receiptId The receipt ID
 * @param itemConfirmations Array of item confirmations with storage locations
 * @param logger Pino logger instance
 * @returns Array of created inventory items
 */
export const addItemsFromReceipt = async (
  userId: string,
  receiptId: number,
  itemConfirmations: Array<{
    receipt_item_id: number;
    item_name?: string;
    quantity?: number;
    location?: StorageLocation;
    expiry_date?: string;
    include: boolean;
  }>,
  logger: Logger,
): Promise<UserInventoryItem[]> => {
  const receiptLogger = logger.child({ userId, receiptId });
  receiptLogger.info(
    { itemCount: itemConfirmations.length },
    'Adding items from receipt to inventory',
  );

  const createdItems: UserInventoryItem[] = [];

  // Get receipt details for purchase date
  const receipt = await receiptRepo.getReceiptById(receiptId, userId, receiptLogger);

  for (const confirmation of itemConfirmations) {
    if (!confirmation.include) {
      receiptLogger.debug(
        { receiptItemId: confirmation.receipt_item_id },
        'Skipping excluded item',
      );
      continue;
    }

    try {
      // Get the receipt item details
      const receiptItems = await receiptRepo.getReceiptItems(receiptId, receiptLogger);
      const receiptItem = receiptItems.find(
        (ri) => ri.receipt_item_id === confirmation.receipt_item_id,
      );

      if (!receiptItem) {
        receiptLogger.warn(
          { receiptItemId: confirmation.receipt_item_id },
          'Receipt item not found',
        );
        continue;
      }

      // Create inventory item
      const inventoryItem = await addInventoryItem(
        userId,
        {
          product_id: receiptItem.product_id ?? undefined,
          master_item_id: receiptItem.master_item_id ?? undefined,
          item_name: confirmation.item_name || receiptItem.raw_item_description,
          quantity: confirmation.quantity || receiptItem.quantity,
          purchase_date: receipt.transaction_date || receipt.created_at.split('T')[0],
          expiry_date: confirmation.expiry_date,
          source: 'receipt_scan',
          location: confirmation.location,
        },
        receiptLogger,
      );

      // Update receipt item to mark as added to pantry
      await receiptRepo.updateReceiptItem(
        confirmation.receipt_item_id,
        {
          added_to_pantry: true,
          pantry_item_id: inventoryItem.inventory_id,
        },
        receiptLogger,
      );

      createdItems.push(inventoryItem);
    } catch (error) {
      const err = error instanceof Error ? error : new Error(String(error));
      receiptLogger.error(
        { err, receiptItemId: confirmation.receipt_item_id },
        'Error adding receipt item to inventory',
      );
    }
  }

  receiptLogger.info({ createdCount: createdItems.length }, 'Items added from receipt');
  return createdItems;
};

/**
 * Gets recipe suggestions based on expiring items.
 * Prioritizes recipes that use items closest to expiry.
 * @param userId The user's ID
 * @param daysAhead Number of days to look ahead for expiring items
 * @param logger Pino logger instance
 * @param options Pagination options
 * @returns Recipes with matching expiring ingredients
 */
export const getRecipeSuggestionsForExpiringItems = async (
  userId: string,
  daysAhead: number,
  logger: Logger,
  options: { limit?: number; offset?: number } = {},
): Promise<{
  recipes: Array<{
    recipe_id: number;
    recipe_name: string;
    description: string | null;
    prep_time_minutes: number | null;
    cook_time_minutes: number | null;
    servings: number | null;
    photo_url: string | null;
    matching_items: UserInventoryItem[];
    match_count: number;
  }>;
  total: number;
  considered_items: UserInventoryItem[];
}> => {
  const { limit = 10, offset = 0 } = options;
  const suggestionLogger = logger.child({ userId, daysAhead });
  suggestionLogger.debug('Fetching recipe suggestions for expiring items');

  // Get expiring items to include in the response
  const expiringItems = await getExpiringItems(userId, daysAhead, logger);

  if (expiringItems.length === 0) {
    suggestionLogger.debug('No expiring items found, returning empty suggestions');
    return {
      recipes: [],
      total: 0,
      considered_items: [],
    };
  }

  // Get recipes that use the expiring items
  const recipeData = await expiryRepo.getRecipesForExpiringItems(
    userId,
    daysAhead,
    limit,
    offset,
    suggestionLogger,
  );

  // Map the expiring items by master_item_id for quick lookup
  const itemsByMasterId = new Map<number, UserInventoryItem>();
  for (const item of expiringItems) {
    if (item.master_item_id && !itemsByMasterId.has(item.master_item_id)) {
      itemsByMasterId.set(item.master_item_id, item);
    }
  }

  // Build the response with matching items
  const recipes = recipeData.recipes.map((recipe) => ({
    recipe_id: recipe.recipe_id,
    recipe_name: recipe.recipe_name,
    description: recipe.description,
    prep_time_minutes: recipe.prep_time_minutes,
    cook_time_minutes: recipe.cook_time_minutes,
    servings: recipe.servings,
    photo_url: recipe.photo_url,
    matching_items: recipe.matching_master_item_ids
      .map((id) => itemsByMasterId.get(id))
      .filter((item): item is UserInventoryItem => item !== undefined),
    match_count: recipe.match_count,
  }));

  suggestionLogger.info(
    {
      recipeCount: recipes.length,
      total: recipeData.total,
      expiringItemCount: expiringItems.length,
    },
    'Recipe suggestions fetched for expiring items',
  );

  return {
    recipes,
    total: recipeData.total,
    considered_items: expiringItems,
  };
};

// ============================================================================
// JOB PROCESSING
// ============================================================================

import type { Job } from 'bullmq';
import type { ExpiryAlertJobData } from '../types/job-data';
import * as emailService from './emailService.server';

/**
 * Processes an expiry alert job from the queue.
 * This is the main entry point for background expiry alert processing.
 * @param job The BullMQ job
 * @param logger Pino logger instance
 * @returns Processing result with counts of alerts sent
 */
export const processExpiryAlertJob = async (
  job: Job<ExpiryAlertJobData>,
  logger: Logger,
): Promise<{ success: boolean; alertsSent: number; usersNotified: number }> => {
  const {
    alertType,
    userId,
    daysAhead = DEFAULT_EXPIRY_WARNING_DAYS,
    scheduledAt: _scheduledAt,
  } = job.data;
  const jobLogger = logger.child({
    jobId: job.id,
    alertType,
    userId,
    daysAhead,
    requestId: job.data.meta?.requestId,
  });

  jobLogger.info('Starting expiry alert job');

  try {
    let alertsSent = 0;
    let usersNotified = 0;

    if (alertType === 'user_specific' && userId) {
      // Process alerts for a single user
      const result = await processUserExpiryAlerts(userId, daysAhead, jobLogger);
      alertsSent = result.alertsSent;
      usersNotified = result.alertsSent > 0 ? 1 : 0;
    } else if (alertType === 'daily_check') {
      // Process daily alerts for all users with expiring items
      const result = await processDailyExpiryAlerts(daysAhead, jobLogger);
      alertsSent = result.totalAlerts;
      usersNotified = result.usersNotified;
    }

    jobLogger.info({ alertsSent, usersNotified }, 'Expiry alert job completed');

    return { success: true, alertsSent, usersNotified };
  } catch (error) {
    jobLogger.error({ err: error }, 'Expiry alert job failed');
    throw error;
  }
};

/**
 * Processes expiry alerts for a single user.
 * @param userId The user's ID
 * @param daysAhead Days ahead to check for expiring items
 * @param logger Pino logger instance
 * @returns Number of alerts sent
 */
const processUserExpiryAlerts = async (
  userId: string,
  daysAhead: number,
  logger: Logger,
): Promise<{ alertsSent: number }> => {
  const userLogger = logger.child({ userId });

  // Get user's alert settings
  const settings = await expiryRepo.getUserAlertSettings(userId, userLogger);
  const enabledSettings = settings.filter((s) => s.is_enabled);

  if (enabledSettings.length === 0) {
    userLogger.debug('No enabled alert settings for user');
    return { alertsSent: 0 };
  }

  // Get expiring items
  const expiringItems = await getExpiringItems(userId, daysAhead, userLogger);

  if (expiringItems.length === 0) {
    userLogger.debug('No expiring items for user');
    return { alertsSent: 0 };
  }

  let alertsSent = 0;

  // Group items by urgency for the alert (kept for future use in alert formatting)
  const _expiredItems = expiringItems.filter((i) => i.expiry_status === 'expired');
  const _soonItems = expiringItems.filter((i) => i.expiry_status === 'expiring_soon');

  // Check if we should send alerts based on settings
  for (const setting of enabledSettings) {
    const relevantItems = expiringItems.filter(
      (item) =>
        item.days_until_expiry !== null && item.days_until_expiry <= setting.days_before_expiry,
    );

    if (relevantItems.length > 0) {
      // Log the alert
      for (const item of relevantItems) {
        const alertType: ExpiryAlertType =
          item.expiry_status === 'expired' ? 'expired' : 'expiring_soon';
        await expiryRepo.logAlert(
          userId,
          alertType,
          setting.alert_method,
          item.item_name,
          userLogger,
          {
            pantryItemId: item.inventory_id,
            expiryDate: item.expiry_date || null,
            daysUntilExpiry: item.days_until_expiry,
          },
        );
        alertsSent++;
      }

      // Update last alert sent time via upsert
      await expiryRepo.upsertAlertSettings(userId, setting.alert_method, {}, userLogger);
    }
  }

  userLogger.info({ alertsSent, itemCount: expiringItems.length }, 'Processed user expiry alerts');
  return { alertsSent };
};

/**
 * Processes daily expiry alerts for all users.
 * @param daysAhead Days ahead to check for expiring items
 * @param logger Pino logger instance
 * @returns Total alerts and users notified
 */
const processDailyExpiryAlerts = async (
  daysAhead: number,
  logger: Logger,
): Promise<{ totalAlerts: number; usersNotified: number }> => {
  // Get all users with items expiring within the threshold
  const usersWithExpiringItems = await expiryRepo.getUsersWithExpiringItems(logger);

  // Get unique user IDs
  const uniqueUserIds = [...new Set(usersWithExpiringItems.map((u) => u.user_id))];

  let totalAlerts = 0;
  let usersNotified = 0;

  for (const userId of uniqueUserIds) {
    try {
      const result = await processUserExpiryAlerts(userId, daysAhead, logger);
      totalAlerts += result.alertsSent;
      if (result.alertsSent > 0) {
        usersNotified++;
      }
    } catch (error) {
      logger.error({ err: error, userId }, 'Failed to process alerts for user');
      // Continue with other users
    }
  }

  logger.info(
    { totalAlerts, usersNotified, totalUsers: uniqueUserIds.length },
    'Daily expiry alert processing complete',
  );

  return { totalAlerts, usersNotified };
};
