/**
 * @ai-summary Core processing service for the email-to-record pipeline
 * @ai-context Orchestrates sender validation, OCR extraction, record classification,
 *   vehicle association, status tracking, and retry logic. Delegates all notifications
 *   (emails, in-app, logging) to EmailIngestionNotificationHandler.
 */

import { Pool } from 'pg';
import pool from '../../../core/config/database';
import { logger } from '../../../core/logging/logger';
import { EmailIngestionRepository } from '../data/email-ingestion.repository';
import { ResendInboundClient, type ParsedEmailAttachment } from '../external/resend-inbound.client';
import { UserProfileRepository } from '../../user-profile/data/user-profile.repository';
import { VehiclesRepository } from '../../vehicles/data/vehicles.repository';
import { NotificationsRepository } from '../../notifications/data/notifications.repository';
import { TemplateService } from '../../notifications/domain/template.service';
import { EmailService } from '../../notifications/domain/email.service';
import { ocrService } from '../../ocr/domain/ocr.service';
import type { ReceiptExtractionResponse } from '../../ocr/domain/ocr.types';
import { ReceiptClassifier } from './receipt-classifier';
import { EmailIngestionNotificationHandler } from './notification-handler';
import { FuelLogsService } from '../../fuel-logs/domain/fuel-logs.service';
import { FuelLogsRepository } from '../../fuel-logs/data/fuel-logs.repository';
import { FuelType } from '../../fuel-logs/domain/fuel-logs.types';
import type { EnhancedCreateFuelLogRequest } from '../../fuel-logs/domain/fuel-logs.types';
import { MaintenanceService } from '../../maintenance/domain/maintenance.service';
import type { MaintenanceCategory } from '../../maintenance/domain/maintenance.types';
import { validateSubtypes, getSubtypesForCategory } from '../../maintenance/domain/maintenance.types';
import type {
  ResendWebhookEvent,
  EmailProcessingResult,
  ExtractedReceiptData,
  EmailRecordType,
} from './email-ingestion.types';

/** Supported attachment MIME types */
const SUPPORTED_ATTACHMENT_TYPES = new Set([
  'application/pdf',
  'image/png',
  'image/jpeg',
  'image/heic',
  'image/heif',
]);

/** Image types that work with receipt-specific OCR */
const OCR_RECEIPT_IMAGE_TYPES = new Set([
  'image/jpeg',
  'image/png',
  'image/heic',
  'image/heif',
]);

const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024; // 10MB
const MAX_RETRY_COUNT = 3;

export class EmailIngestionService {
  private repository: EmailIngestionRepository;
  private resendClient: ResendInboundClient;
  private userProfileRepository: UserProfileRepository;
  private vehiclesRepository: VehiclesRepository;
  private notificationHandler: EmailIngestionNotificationHandler;
  private classifier: ReceiptClassifier;
  private fuelLogsService: FuelLogsService;
  private maintenanceService: MaintenanceService;

  constructor(dbPool?: Pool) {
    const p = dbPool || pool;
    this.repository = new EmailIngestionRepository(p);
    this.resendClient = new ResendInboundClient();
    this.userProfileRepository = new UserProfileRepository(p);
    this.vehiclesRepository = new VehiclesRepository(p);
    const notificationsRepository = new NotificationsRepository(p);
    this.notificationHandler = new EmailIngestionNotificationHandler(
      notificationsRepository,
      new TemplateService(),
      new EmailService(),
    );
    this.classifier = new ReceiptClassifier();
    this.fuelLogsService = new FuelLogsService(new FuelLogsRepository(p));
    this.maintenanceService = new MaintenanceService();
  }

  // ========================
  // Main Processing Pipeline
  // ========================

  /**
   * Process an inbound email through the full pipeline.
   * Called asynchronously after webhook receipt is acknowledged.
   */
  async processEmail(emailId: string, event: ResendWebhookEvent): Promise<void> {
    const senderEmail = event.data.from;
    const subject = event.data.subject;

    try {
      // 1. Mark as processing
      await this.repository.updateQueueStatus(emailId, 'processing');

      // 2. Validate sender
      const userProfile = await this.validateSender(senderEmail);
      if (!userProfile) {
        await this.handleUnregisteredSender(emailId, senderEmail);
        return;
      }

      const userId = userProfile.auth0Sub;
      const userName = userProfile.displayName || userProfile.email;

      // Update queue with resolved user_id
      await this.repository.updateQueueStatus(emailId, 'processing', { userId });

      // 3. Get attachments (from webhook data or by fetching raw email)
      const attachments = await this.getAttachments(emailId, event);

      // 4. Filter valid attachments
      const validAttachments = this.filterAttachments(attachments);
      if (validAttachments.length === 0) {
        await this.handleNoValidAttachments(emailId, userId, userName, senderEmail);
        return;
      }

      // 5. Classify receipt from email text first
      const emailClassification = this.classifier.classifyFromText(subject, event.data.text);
      logger.info('Email text classification result', {
        emailId,
        type: emailClassification.type,
        confidence: emailClassification.confidence,
      });

      // 6. Process attachments through OCR using classification
      const ocrResult = await this.processAttachmentsWithClassification(
        userId, validAttachments, emailClassification, emailId
      );
      if (!ocrResult) {
        await this.handleOcrFailure(emailId, userId, userName, senderEmail, 'No receipt data could be extracted from attachments');
        return;
      }

      // 7. Build extracted data from OCR result
      const extractedData = this.mapOcrToExtractedData(ocrResult.response);
      const recordType = ocrResult.recordType;

      // 8. Handle vehicle association
      const processingResult = await this.handleVehicleAssociation(
        userId, userName, senderEmail, recordType, extractedData
      );

      // 9. Mark as completed
      await this.repository.updateQueueStatus(emailId, 'completed', {
        processingResult,
      });

      logger.info('Email processing completed successfully', {
        emailId,
        userId,
        recordType,
        vehicleId: processingResult.vehicleId,
        pendingAssociationId: processingResult.pendingAssociationId,
      });
    } catch (error) {
      await this.handleProcessingError(emailId, senderEmail, subject, error);
    }
  }

  // ========================
  // Sender Validation
  // ========================

  private async validateSender(senderEmail: string): Promise<{
    auth0Sub: string;
    email: string;
    displayName: string | null;
  } | null> {
    // Case-insensitive lookup by lowercasing the sender email
    const profile = await this.userProfileRepository.getByEmail(senderEmail.toLowerCase());
    if (profile) {
      return {
        auth0Sub: profile.auth0Sub,
        email: profile.email,
        displayName: profile.displayName ?? null,
      };
    }

    // Try original case as fallback
    if (senderEmail !== senderEmail.toLowerCase()) {
      const fallback = await this.userProfileRepository.getByEmail(senderEmail);
      if (fallback) {
        return {
          auth0Sub: fallback.auth0Sub,
          email: fallback.email,
          displayName: fallback.displayName ?? null,
        };
      }
    }

    return null;
  }

  // ========================
  // Attachment Handling
  // ========================

  /**
   * Get attachments from webhook data or by fetching the raw email
   */
  private async getAttachments(
    emailId: string,
    event: ResendWebhookEvent
  ): Promise<ParsedEmailAttachment[]> {
    // If webhook includes attachments with content, use those
    if (event.data.attachments && event.data.attachments.length > 0) {
      return event.data.attachments.map(att => ({
        filename: att.filename,
        contentType: att.content_type,
        content: Buffer.from(att.content, 'base64'),
        size: Buffer.from(att.content, 'base64').length,
      }));
    }

    // Otherwise fetch and parse the raw email
    try {
      const { downloadUrl } = await this.resendClient.getEmail(emailId);
      const rawEmail = await this.resendClient.downloadRawEmail(downloadUrl);
      const parsed = await this.resendClient.parseEmail(rawEmail);
      return parsed.attachments;
    } catch (error) {
      logger.warn('Failed to fetch raw email for attachments', {
        emailId,
        error: error instanceof Error ? error.message : String(error),
      });
      return [];
    }
  }

  /**
   * Filter attachments by supported type and size
   */
  private filterAttachments(attachments: ParsedEmailAttachment[]): ParsedEmailAttachment[] {
    return attachments.filter(att => {
      if (!SUPPORTED_ATTACHMENT_TYPES.has(att.contentType)) {
        logger.info('Skipping unsupported attachment type', {
          filename: att.filename,
          contentType: att.contentType,
        });
        return false;
      }

      if (att.size > MAX_ATTACHMENT_SIZE) {
        logger.info('Skipping oversized attachment', {
          filename: att.filename,
          size: att.size,
          maxSize: MAX_ATTACHMENT_SIZE,
        });
        return false;
      }

      return true;
    });
  }

  // ========================
  // OCR Processing
  // ========================

  /**
   * Process attachments using classifier-driven OCR extraction.
   * If email text classification is confident, calls the specific OCR endpoint.
   * If not, performs general OCR and classifies from rawText.
   * Returns null if no usable result or receipt is unclassified.
   */
  private async processAttachmentsWithClassification(
    userId: string,
    attachments: ParsedEmailAttachment[],
    emailClassification: { type: string; confidence: number },
    emailId: string
  ): Promise<{ response: ReceiptExtractionResponse; recordType: EmailRecordType } | null> {
    const imageAttachments = attachments.filter(att => OCR_RECEIPT_IMAGE_TYPES.has(att.contentType));

    for (const attachment of imageAttachments) {
      // If email text gave a confident classification, call the specific OCR endpoint first
      if (emailClassification.type === 'fuel') {
        const result = await this.extractFuelReceipt(userId, attachment);
        if (result?.success) return { response: result, recordType: 'fuel_log' };
        // Fuel OCR failed, try maintenance as fallback
        const fallbackResult = await this.extractMaintenanceReceipt(userId, attachment);
        if (fallbackResult?.success) return { response: fallbackResult, recordType: 'maintenance_record' };
        continue;
      }

      if (emailClassification.type === 'maintenance') {
        const result = await this.extractMaintenanceReceipt(userId, attachment);
        if (result?.success) return { response: result, recordType: 'maintenance_record' };
        // Maintenance OCR failed, try fuel as fallback
        const fallbackResult = await this.extractFuelReceipt(userId, attachment);
        if (fallbackResult?.success) return { response: fallbackResult, recordType: 'fuel_log' };
        continue;
      }

      // Email text was not confident - try both OCR endpoints and classify from rawText
      const fuelResult = await this.extractFuelReceipt(userId, attachment);
      const maintenanceResult = await this.extractMaintenanceReceipt(userId, attachment);

      // Use rawText from whichever succeeded for secondary classification
      const rawText = fuelResult?.rawText || maintenanceResult?.rawText || '';
      if (rawText) {
        const ocrClassification = this.classifier.classifyFromOcrRawText(rawText);
        logger.info('OCR rawText classification result', {
          emailId,
          type: ocrClassification.type,
          confidence: ocrClassification.confidence,
        });

        if (ocrClassification.type === 'fuel' && fuelResult?.success) {
          return { response: fuelResult, recordType: 'fuel_log' };
        }
        if (ocrClassification.type === 'maintenance' && maintenanceResult?.success) {
          return { response: maintenanceResult, recordType: 'maintenance_record' };
        }
      }

      // Both classifiers failed - fall back to field-count heuristic
      const fallback = this.selectBestResultByFields(fuelResult, maintenanceResult);
      if (fallback) return fallback;
    }

    return null;
  }

  /**
   * Extract fuel receipt via OCR. Returns null on failure.
   */
  private async extractFuelReceipt(
    userId: string,
    attachment: ParsedEmailAttachment
  ): Promise<ReceiptExtractionResponse | null> {
    try {
      return await ocrService.extractReceipt(userId, {
        fileBuffer: attachment.content,
        contentType: attachment.contentType,
        receiptType: 'fuel',
      });
    } catch (error) {
      logger.info('Fuel receipt extraction failed', {
        filename: attachment.filename,
        error: error instanceof Error ? error.message : String(error),
      });
      return null;
    }
  }

  /**
   * Extract maintenance receipt via OCR. Returns null on failure.
   */
  private async extractMaintenanceReceipt(
    userId: string,
    attachment: ParsedEmailAttachment
  ): Promise<ReceiptExtractionResponse | null> {
    try {
      return await ocrService.extractMaintenanceReceipt(userId, {
        fileBuffer: attachment.content,
        contentType: attachment.contentType,
      });
    } catch (error) {
      logger.info('Maintenance receipt extraction failed', {
        filename: attachment.filename,
        error: error instanceof Error ? error.message : String(error),
      });
      return null;
    }
  }

  /**
   * Last-resort fallback: select the better OCR result based on domain-specific
   * fields and field count when keyword classifiers could not decide.
   */
  private selectBestResultByFields(
    fuelResult: ReceiptExtractionResponse | null,
    maintenanceResult: ReceiptExtractionResponse | null
  ): { response: ReceiptExtractionResponse; recordType: EmailRecordType } | null {
    const fuelFieldCount = fuelResult?.success
      ? Object.keys(fuelResult.extractedFields).length
      : 0;
    const maintenanceFieldCount = maintenanceResult?.success
      ? Object.keys(maintenanceResult.extractedFields).length
      : 0;

    if (fuelFieldCount === 0 && maintenanceFieldCount === 0) {
      return null;
    }

    const hasFuelFields = fuelResult?.extractedFields['gallons'] ||
      fuelResult?.extractedFields['price_per_gallon'] ||
      fuelResult?.extractedFields['fuel_type'];

    const hasMaintenanceFields = maintenanceResult?.extractedFields['category'] ||
      maintenanceResult?.extractedFields['shop_name'] ||
      maintenanceResult?.extractedFields['description'];

    if (hasFuelFields && !hasMaintenanceFields) {
      return { response: fuelResult!, recordType: 'fuel_log' };
    }
    if (hasMaintenanceFields && !hasFuelFields) {
      return { response: maintenanceResult!, recordType: 'maintenance_record' };
    }

    if (fuelFieldCount >= maintenanceFieldCount && fuelResult?.success) {
      return { response: fuelResult, recordType: 'fuel_log' };
    }
    if (maintenanceResult?.success) {
      return { response: maintenanceResult, recordType: 'maintenance_record' };
    }

    return null;
  }

  /**
   * Map OCR extracted fields to our ExtractedReceiptData format
   */
  private mapOcrToExtractedData(response: ReceiptExtractionResponse): ExtractedReceiptData {
    const fields = response.extractedFields;
    const getFieldValue = (key: string): string | null =>
      fields[key]?.value || null;
    const getFieldNumber = (key: string): number | null => {
      const val = fields[key]?.value;
      if (!val) return null;
      const num = parseFloat(val);
      return isNaN(num) ? null : num;
    };

    return {
      vendor: getFieldValue('vendor') || getFieldValue('shop_name'),
      date: getFieldValue('date'),
      total: getFieldNumber('total'),
      odometerReading: getFieldNumber('odometer') || getFieldNumber('odometer_reading'),
      gallons: getFieldNumber('gallons'),
      pricePerGallon: getFieldNumber('price_per_gallon'),
      fuelType: getFieldValue('fuel_type'),
      category: getFieldValue('category'),
      subtypes: fields['subtypes']?.value ? fields['subtypes'].value.split(',').map(s => s.trim()) : null,
      shopName: getFieldValue('shop_name'),
      description: getFieldValue('description'),
    };
  }

  // ========================
  // Vehicle Association
  // ========================

  /**
   * Handle vehicle association based on user's vehicle count.
   * No vehicles: send error email.
   * Single vehicle: auto-associate and create record.
   * Multiple vehicles: create pending association for user selection.
   */
  private async handleVehicleAssociation(
    userId: string,
    userName: string,
    userEmail: string,
    recordType: EmailRecordType,
    extractedData: ExtractedReceiptData
  ): Promise<EmailProcessingResult> {
    const vehicles = await this.vehiclesRepository.findByUserId(userId);

    // No vehicles: user must add a vehicle first
    if (vehicles.length === 0) {
      await this.notificationHandler.notifyNoVehicles(userId, userName, userEmail);
      return {
        recordType,
        vehicleId: null,
        recordId: null,
        documentId: null,
        pendingAssociationId: null,
        extractedData,
      };
    }

    // Single vehicle: auto-associate and create record
    if (vehicles.length === 1) {
      const vehicle = vehicles[0];
      let recordId: string | null = null;

      try {
        recordId = await this.createRecord(userId, vehicle.id, recordType, extractedData);
      } catch (error) {
        logger.error('Failed to create record from email receipt', {
          userId,
          vehicleId: vehicle.id,
          recordType,
          error: error instanceof Error ? error.message : String(error),
        });
      }

      const vehicleName = vehicle.nickname
        || [vehicle.year, vehicle.make, vehicle.model].filter(Boolean).join(' ')
        || 'your vehicle';

      await this.notificationHandler.notifyReceiptProcessed({
        userId,
        userName,
        userEmail,
        vehicleName,
        recordType,
        recordId,
        vehicleId: vehicle.id,
        extractedData,
      });

      return {
        recordType,
        vehicleId: vehicle.id,
        recordId,
        documentId: null,
        pendingAssociationId: null,
        extractedData,
      };
    }

    // Multiple vehicles: create pending association for user selection
    const pendingAssociation = await this.repository.insertPendingAssociation({
      userId,
      recordType,
      extractedData,
      documentId: null,
    });

    await this.notificationHandler.notifyPendingVehicleSelection({
      userId,
      userName,
      userEmail,
      recordType,
      pendingAssociationId: pendingAssociation.id,
      extractedData,
    });

    return {
      recordType,
      vehicleId: null,
      recordId: null,
      documentId: null,
      pendingAssociationId: pendingAssociation.id,
      extractedData,
    };
  }

  // ========================
  // Public Resolution API
  // ========================

  /**
   * Resolve a pending vehicle association by creating the record with the selected vehicle.
   * Called from the user-facing API when a multi-vehicle user picks a vehicle.
   */
  async resolveAssociation(
    associationId: string,
    vehicleId: string,
    userId: string
  ): Promise<{ recordId: string; recordType: EmailRecordType }> {
    const association = await this.repository.getPendingAssociationById(associationId);
    if (!association) {
      throw new Error('Pending association not found');
    }
    if (association.userId !== userId) {
      throw new Error('Unauthorized');
    }
    if (association.status !== 'pending') {
      throw new Error('Association already resolved');
    }

    // Verify vehicle belongs to user
    const vehicles = await this.vehiclesRepository.findByUserId(userId);
    const vehicle = vehicles.find(v => v.id === vehicleId);
    if (!vehicle) {
      throw new Error('Vehicle not found');
    }

    // Create the record
    const recordId = await this.createRecord(userId, vehicleId, association.recordType, association.extractedData);

    // Mark as resolved
    await this.repository.resolvePendingAssociation(associationId, 'resolved');

    logger.info('Pending association resolved', { associationId, vehicleId, userId, recordType: association.recordType, recordId });

    return { recordId, recordType: association.recordType };
  }

  /**
   * Dismiss a pending vehicle association without creating a record.
   */
  async dismissAssociation(associationId: string, userId: string): Promise<void> {
    const association = await this.repository.getPendingAssociationById(associationId);
    if (!association) {
      throw new Error('Pending association not found');
    }
    if (association.userId !== userId) {
      throw new Error('Unauthorized');
    }
    if (association.status !== 'pending') {
      throw new Error('Association already resolved');
    }

    await this.repository.resolvePendingAssociation(associationId, 'expired');
    logger.info('Pending association dismissed', { associationId, userId });
  }

  // ========================
  // Record Creation
  // ========================

  /**
   * Create a fuel log or maintenance record from extracted receipt data.
   * Returns the created record ID.
   */
  private async createRecord(
    userId: string,
    vehicleId: string,
    recordType: EmailRecordType,
    extractedData: ExtractedReceiptData
  ): Promise<string> {
    if (recordType === 'fuel_log') {
      return this.createFuelLogRecord(userId, vehicleId, extractedData);
    }
    return this.createMaintenanceRecord(userId, vehicleId, extractedData);
  }

  /**
   * Map extracted receipt data to EnhancedCreateFuelLogRequest and create fuel log.
   */
  private async createFuelLogRecord(
    userId: string,
    vehicleId: string,
    data: ExtractedReceiptData
  ): Promise<string> {
    const fuelUnits = data.gallons ?? 0;
    const costPerUnit = data.pricePerGallon ?? (data.total && fuelUnits > 0 ? data.total / fuelUnits : 0);

    const request: EnhancedCreateFuelLogRequest = {
      vehicleId,
      dateTime: data.date || new Date().toISOString(),
      fuelType: this.mapFuelType(data.fuelType),
      fuelUnits,
      costPerUnit,
      odometerReading: data.odometerReading ?? undefined,
      locationData: data.vendor ? { stationName: data.vendor } : undefined,
      notes: 'Created from emailed receipt',
    };

    logger.info('Creating fuel log from email receipt', { userId, vehicleId, fuelUnits, costPerUnit });
    const result = await this.fuelLogsService.createFuelLog(request, userId);
    return result.id;
  }

  /**
   * Map extracted receipt data to CreateMaintenanceRecordRequest and create maintenance record.
   */
  private async createMaintenanceRecord(
    userId: string,
    vehicleId: string,
    data: ExtractedReceiptData
  ): Promise<string> {
    const category = this.mapMaintenanceCategory(data.category);
    const subtypes = this.resolveMaintenanceSubtypes(category, data.subtypes);

    const record = await this.maintenanceService.createRecord(userId, {
      vehicleId,
      category,
      subtypes,
      date: data.date || new Date().toISOString().split('T')[0],
      odometerReading: data.odometerReading ?? undefined,
      cost: data.total ?? undefined,
      shopName: data.shopName || data.vendor || undefined,
      notes: data.description
        ? `${data.description}\n\nCreated from emailed receipt`
        : 'Created from emailed receipt',
    });

    logger.info('Created maintenance record from email receipt', { userId, vehicleId, recordId: record.id, category });
    return record.id;
  }

  /**
   * Map OCR fuel type string to FuelType enum. Defaults to gasoline.
   */
  private mapFuelType(fuelTypeStr: string | null): FuelType {
    if (!fuelTypeStr) return FuelType.GASOLINE;

    const normalized = fuelTypeStr.toLowerCase().trim();
    if (normalized.includes('diesel') || normalized === '#1' || normalized === '#2') {
      return FuelType.DIESEL;
    }
    if (normalized.includes('electric') || normalized.includes('ev')) {
      return FuelType.ELECTRIC;
    }
    return FuelType.GASOLINE;
  }

  /**
   * Map OCR category string to MaintenanceCategory. Defaults to routine_maintenance.
   */
  private mapMaintenanceCategory(categoryStr: string | null): MaintenanceCategory {
    if (!categoryStr) return 'routine_maintenance';

    const normalized = categoryStr.toLowerCase().trim();
    if (normalized.includes('repair')) return 'repair';
    if (normalized.includes('performance') || normalized.includes('upgrade')) return 'performance_upgrade';
    return 'routine_maintenance';
  }

  /**
   * Validate and resolve maintenance subtypes. Falls back to first valid
   * subtype for the category if OCR subtypes are invalid or missing.
   */
  private resolveMaintenanceSubtypes(
    category: MaintenanceCategory,
    ocrSubtypes: string[] | null
  ): string[] {
    if (ocrSubtypes && ocrSubtypes.length > 0 && validateSubtypes(category, ocrSubtypes)) {
      return ocrSubtypes;
    }

    // Attempt to match OCR subtypes against valid options (case-insensitive)
    if (ocrSubtypes && ocrSubtypes.length > 0) {
      const validOptions = getSubtypesForCategory(category);
      const matched = ocrSubtypes
        .map(s => validOptions.find(v => v.toLowerCase() === s.toLowerCase().trim()))
        .filter((v): v is string => v !== undefined);
      if (matched.length > 0) return matched;
    }

    // Default to first subtype of category
    const defaults = getSubtypesForCategory(category);
    return [defaults[0] as string];
  }

  // ========================
  // Error Handling & Retries
  // ========================

  private async handleProcessingError(
    emailId: string,
    senderEmail: string,
    _subject: string | null,
    error: unknown
  ): Promise<void> {
    const errorMessage = error instanceof Error ? error.message : String(error);
    logger.error('Email processing pipeline error', { emailId, error: errorMessage });

    // Get current queue entry for retry count and userId
    const queueEntry = await this.repository.getQueueEntry(emailId);
    const currentRetryCount = queueEntry?.retryCount || 0;
    const newRetryCount = currentRetryCount + 1;

    if (newRetryCount < MAX_RETRY_COUNT) {
      // Mark for retry
      await this.repository.updateQueueStatus(emailId, 'failed', {
        errorMessage,
        retryCount: newRetryCount,
      });

      logger.info('Email queued for retry', {
        emailId,
        retryCount: newRetryCount,
        maxRetries: MAX_RETRY_COUNT,
      });
    } else {
      // Max retries exceeded - permanently failed
      await this.repository.updateQueueStatus(emailId, 'failed', {
        errorMessage: `Max retries (${MAX_RETRY_COUNT}) exceeded. Last error: ${errorMessage}`,
        retryCount: newRetryCount,
      });

      // Send failure notification (email + in-app if userId available)
      await this.notificationHandler.notifyProcessingFailure({
        userId: queueEntry?.userId,
        userEmail: senderEmail,
        errorReason: errorMessage,
      }).catch(notifyErr => {
        logger.error('Failed to send failure notification', {
          emailId,
          error: notifyErr instanceof Error ? notifyErr.message : String(notifyErr),
        });
      });
    }
  }

  private async handleUnregisteredSender(
    emailId: string,
    senderEmail: string
  ): Promise<void> {
    logger.info('Unregistered sender rejected', { emailId, senderEmail });

    await this.repository.updateQueueStatus(emailId, 'failed', {
      errorMessage: 'Sender email is not registered with MotoVaultPro',
    });

    await this.notificationHandler.notifyUnregisteredSender(senderEmail).catch(error => {
      logger.error('Failed to send unregistered sender notification', {
        emailId,
        error: error instanceof Error ? error.message : String(error),
      });
    });
  }

  private async handleNoValidAttachments(
    emailId: string,
    userId: string,
    userName: string,
    userEmail: string
  ): Promise<void> {
    logger.info('No valid attachments found', { emailId });

    await this.repository.updateQueueStatus(emailId, 'failed', {
      errorMessage: 'No valid attachments found. Supported types: PDF, PNG, JPG, JPEG, HEIC (max 10MB each)',
    });

    await this.notificationHandler.notifyNoValidAttachments(userId, userName, userEmail).catch(error => {
      logger.error('Failed to send no-attachments notification', {
        emailId,
        error: error instanceof Error ? error.message : String(error),
      });
    });
  }

  private async handleOcrFailure(
    emailId: string,
    userId: string,
    userName: string,
    userEmail: string,
    reason: string
  ): Promise<void> {
    logger.info('OCR extraction failed for all attachments', { emailId, reason });

    await this.repository.updateQueueStatus(emailId, 'failed', {
      errorMessage: reason,
    });

    await this.notificationHandler.notifyOcrFailure(userId, userName, userEmail, reason).catch(error => {
      logger.error('Failed to send OCR failure notification', {
        emailId,
        error: error instanceof Error ? error.message : String(error),
      });
    });
  }
}
