// src/services/receiptService.server.ts
/**
 * @file Receipt Scanning Service
 * Handles receipt image processing, OCR extraction, item parsing, and store detection.
 * Integrates with expiry tracking for adding scanned items to inventory.
 */
import type { Logger } from 'pino';
import { receiptRepo } from './db/index.db';
import type {
  OcrProvider,
  ReceiptScan,
  ReceiptItem,
  ReceiptProcessingLogRecord,
} from '../types/expiry';
import type { UpdateReceiptItemRequest, ReceiptQueryOptions } from './db/receipt.db';

/**
 * Maximum number of retry attempts for failed receipt processing
 */
const MAX_RETRY_ATTEMPTS = 3;

/**
 * Default OCR provider to use
 */
const DEFAULT_OCR_PROVIDER: OcrProvider = 'internal';

// ============================================================================
// RECEIPT MANAGEMENT
// ============================================================================

/**
 * Creates a new receipt record for processing.
 * @param userId The user's ID
 * @param imageUrl URL or path to the receipt image
 * @param logger Pino logger instance
 * @param options Optional store ID and transaction date if known
 * @returns The created receipt record
 */
export const createReceipt = async (
  userId: string,
  imageUrl: string,
  logger: Logger,
  options: { storeId?: number; transactionDate?: string } = {},
): Promise<ReceiptScan> => {
  logger.info({ userId, imageUrl }, 'Creating new receipt for processing');

  const receipt = await receiptRepo.createReceipt(
    {
      user_id: userId,
      receipt_image_url: imageUrl,
      store_id: options.storeId,
      transaction_date: options.transactionDate,
    },
    logger,
  );

  // Log the upload step
  await receiptRepo.logProcessingStep(receipt.receipt_id, 'upload', 'completed', logger, {
    provider: DEFAULT_OCR_PROVIDER,
  });

  return receipt;
};

/**
 * Gets a receipt by ID.
 * @param receiptId The receipt ID
 * @param userId The user's ID (for authorization)
 * @param logger Pino logger instance
 * @returns The receipt record
 */
export const getReceiptById = async (
  receiptId: number,
  userId: string,
  logger: Logger,
): Promise<ReceiptScan> => {
  return receiptRepo.getReceiptById(receiptId, userId, logger);
};

/**
 * Gets receipts for a user with optional filtering.
 * @param options Query options
 * @param logger Pino logger instance
 * @returns Paginated receipts
 */
export const getReceipts = async (
  options: ReceiptQueryOptions,
  logger: Logger,
): Promise<{ receipts: ReceiptScan[]; total: number }> => {
  logger.debug({ userId: options.user_id }, 'Fetching receipts');
  return receiptRepo.getReceipts(options, logger);
};

/**
 * Deletes a receipt and all associated data.
 * @param receiptId The receipt ID
 * @param userId The user's ID (for authorization)
 * @param logger Pino logger instance
 */
export const deleteReceipt = async (
  receiptId: number,
  userId: string,
  logger: Logger,
): Promise<void> => {
  logger.info({ receiptId, userId }, 'Deleting receipt');
  await receiptRepo.deleteReceipt(receiptId, userId, logger);
};

// ============================================================================
// RECEIPT PROCESSING
// ============================================================================

/**
 * Processes a receipt through OCR and item extraction.
 * This is the main entry point for receipt processing, typically called by a worker.
 * @param receiptId The receipt ID to process
 * @param logger Pino logger instance
 * @returns The processed receipt with extracted items
 */
export const processReceipt = async (
  receiptId: number,
  logger: Logger,
): Promise<{ receipt: ReceiptScan; items: ReceiptItem[] }> => {
  const processLogger = logger.child({ receiptId });
  processLogger.info('Starting receipt processing');

  const startTime = Date.now();

  try {
    // Update status to processing
    let receipt = await receiptRepo.updateReceipt(
      receiptId,
      { status: 'processing' },
      processLogger,
    );

    // Step 1: OCR Extraction
    processLogger.debug('Starting OCR extraction');
    const ocrResult = await performOcrExtraction(receipt.receipt_image_url, processLogger);

    await receiptRepo.logProcessingStep(receiptId, 'ocr_extraction', 'completed', processLogger, {
      provider: ocrResult.provider,
      durationMs: ocrResult.durationMs,
      outputData: { textLength: ocrResult.text.length, confidence: ocrResult.confidence },
    });

    // Update receipt with OCR results
    receipt = await receiptRepo.updateReceipt(
      receiptId,
      {
        raw_text: ocrResult.text,
        ocr_provider: ocrResult.provider,
        ocr_confidence: ocrResult.confidence,
      },
      processLogger,
    );

    // Step 2: Store Detection (if not already set)
    if (!receipt.store_id) {
      processLogger.debug('Attempting store detection');
      const storeDetection = await receiptRepo.detectStoreFromText(ocrResult.text, processLogger);

      if (storeDetection) {
        receipt = await receiptRepo.updateReceipt(
          receiptId,
          {
            store_id: storeDetection.store_id,
            store_confidence: storeDetection.confidence,
          },
          processLogger,
        );

        await receiptRepo.logProcessingStep(
          receiptId,
          'store_detection',
          'completed',
          processLogger,
          {
            outputData: { storeId: storeDetection.store_id, confidence: storeDetection.confidence },
          },
        );
      } else {
        await receiptRepo.logProcessingStep(
          receiptId,
          'store_detection',
          'completed',
          processLogger,
          {
            outputData: { storeId: null, message: 'No store match found' },
          },
        );
      }
    }

    // Step 3: Parse receipt text and extract items
    // If AI extracted items directly, use those; otherwise fall back to text parsing
    processLogger.debug('Starting text parsing and item extraction');
    const parseStartTime = Date.now();

    let itemsToAdd: Array<{
      receipt_id: number;
      raw_item_description: string;
      quantity: number;
      price_paid_cents: number;
      line_number: number;
      is_discount: boolean;
      unit_price_cents?: number;
      unit_type?: string;
    }>;

    if (ocrResult.extractedItems && ocrResult.extractedItems.length > 0) {
      // Use AI-extracted items directly (more accurate)
      processLogger.info(
        { itemCount: ocrResult.extractedItems.length },
        'Using AI-extracted items directly',
      );
      itemsToAdd = ocrResult.extractedItems.map((item, index) => ({
        receipt_id: receiptId,
        raw_item_description: item.raw_item_description,
        quantity: 1, // AI doesn't extract quantity separately yet
        price_paid_cents: item.price_paid_cents,
        line_number: index + 1,
        is_discount: item.price_paid_cents < 0,
      }));
    } else {
      // Fall back to text parsing
      const extractedItems = await parseReceiptText(ocrResult.text, processLogger);
      itemsToAdd = extractedItems.map((item) => ({
        receipt_id: receiptId,
        raw_item_description: item.description,
        quantity: item.quantity,
        price_paid_cents: item.priceCents,
        line_number: item.lineNumber,
        is_discount: item.isDiscount,
        unit_price_cents: item.unitPriceCents,
        unit_type: item.unitType,
      }));
    }

    await receiptRepo.logProcessingStep(receiptId, 'text_parsing', 'completed', processLogger, {
      durationMs: Date.now() - parseStartTime,
      outputData: { itemCount: itemsToAdd.length, usedAiExtraction: !!ocrResult.extractedItems },
    });

    // Step 4: Add extracted items to database
    const items = await receiptRepo.addReceiptItems(itemsToAdd, processLogger);

    await receiptRepo.logProcessingStep(receiptId, 'item_extraction', 'completed', processLogger, {
      outputData: { itemsAdded: items.length },
    });

    // Step 5: Extract total and transaction date
    const receiptMetadata = extractReceiptMetadata(ocrResult.text, processLogger);

    if (receiptMetadata.totalCents || receiptMetadata.transactionDate) {
      receipt = await receiptRepo.updateReceipt(
        receiptId,
        {
          total_amount_cents: receiptMetadata.totalCents,
          transaction_date: receiptMetadata.transactionDate,
        },
        processLogger,
      );
    }

    // Step 6: Mark as completed
    receipt = await receiptRepo.updateReceipt(
      receiptId,
      {
        status: 'completed',
        processed_at: new Date().toISOString(),
      },
      processLogger,
    );

    await receiptRepo.logProcessingStep(receiptId, 'finalization', 'completed', processLogger, {
      durationMs: Date.now() - startTime,
      outputData: { totalItems: items.length, status: 'completed' },
    });

    processLogger.info(
      { receiptId, itemCount: items.length, durationMs: Date.now() - startTime },
      'Receipt processing completed successfully',
    );

    return { receipt, items };
  } catch (error) {
    const err = error instanceof Error ? error : new Error(String(error));
    processLogger.error({ err, receiptId }, 'Receipt processing failed');

    // Increment retry count and update status
    const _retryCount = await receiptRepo.incrementRetryCount(receiptId, processLogger);

    await receiptRepo.updateReceipt(
      receiptId,
      {
        status: 'failed',
        error_details: {
          message: err.message,
          stack: err.stack,
          timestamp: new Date().toISOString(),
        },
      },
      processLogger,
    );

    await receiptRepo.logProcessingStep(receiptId, 'finalization', 'failed', processLogger, {
      durationMs: Date.now() - startTime,
      errorMessage: err.message,
    });

    throw err;
  }
};

/**
 * Performs OCR extraction on a receipt image using Gemini Vision API.
 * Falls back to basic text extraction if AI is not configured.
 * @param imageUrl URL or path to the receipt image
 * @param logger Pino logger instance
 * @returns OCR extraction result
 */
const performOcrExtraction = async (
  imageUrl: string,
  logger: Logger,
): Promise<{
  text: string;
  provider: OcrProvider;
  confidence: number;
  durationMs: number;
  extractedItems?: Array<{ raw_item_description: string; price_paid_cents: number }>;
}> => {
  const startTime = Date.now();

  // Check if AI services are configured
  if (!isAiConfigured) {
    logger.warn({ imageUrl }, 'AI not configured - OCR extraction unavailable');
    return {
      text: '[AI not configured - please set GEMINI_API_KEY]',
      provider: 'internal',
      confidence: 0,
      durationMs: Date.now() - startTime,
    };
  }

  try {
    // Determine if imageUrl is a local file path or URL
    const isLocalPath = !imageUrl.startsWith('http');

    if (!isLocalPath) {
      logger.warn({ imageUrl }, 'Remote URLs not yet supported for OCR - use local file path');
      return {
        text: '[Remote URL OCR not yet implemented - upload file directly]',
        provider: 'internal',
        confidence: 0,
        durationMs: Date.now() - startTime,
      };
    }

    // Determine MIME type from extension
    const ext = path.extname(imageUrl).toLowerCase();
    const mimeTypeMap: Record<string, string> = {
      '.jpg': 'image/jpeg',
      '.jpeg': 'image/jpeg',
      '.png': 'image/png',
      '.gif': 'image/gif',
      '.webp': 'image/webp',
    };
    const mimeType = mimeTypeMap[ext] || 'image/jpeg';

    // Verify file exists
    try {
      await fs.access(imageUrl);
    } catch {
      logger.error({ imageUrl }, 'Receipt image file not found');
      return {
        text: '[Receipt image file not found]',
        provider: 'internal',
        confidence: 0,
        durationMs: Date.now() - startTime,
      };
    }

    logger.info({ imageUrl, mimeType }, 'Starting OCR extraction with Gemini Vision');

    // Use the AI service to extract items from the receipt
    const extractedItems = await aiService.extractItemsFromReceiptImage(imageUrl, mimeType, logger);

    if (!extractedItems || extractedItems.length === 0) {
      logger.warn({ imageUrl }, 'No items extracted from receipt image');
      return {
        text: '[No text could be extracted from receipt]',
        provider: 'gemini',
        confidence: 0.3,
        durationMs: Date.now() - startTime,
      };
    }

    // Convert extracted items to text representation for storage
    const textLines = extractedItems.map(
      (item) => `${item.raw_item_description} - $${(item.price_paid_cents / 100).toFixed(2)}`,
    );
    const extractedText = textLines.join('\n');

    logger.info(
      { imageUrl, itemCount: extractedItems.length, durationMs: Date.now() - startTime },
      'OCR extraction completed successfully',
    );

    return {
      text: extractedText,
      provider: 'gemini',
      confidence: 0.85,
      durationMs: Date.now() - startTime,
      extractedItems, // Pass along for direct use
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    logger.error({ err: error, imageUrl }, 'OCR extraction failed');

    return {
      text: `[OCR extraction failed: ${errorMessage}]`,
      provider: 'internal',
      confidence: 0,
      durationMs: Date.now() - startTime,
    };
  }
};

/**
 * Parses receipt text to extract individual line items.
 * @param text Raw OCR text from receipt
 * @param logger Pino logger instance
 * @returns Array of extracted items
 */
const parseReceiptText = async (
  text: string,
  logger: Logger,
): Promise<
  Array<{
    description: string;
    quantity: number;
    priceCents: number;
    lineNumber: number;
    isDiscount: boolean;
    unitPriceCents?: number;
    unitType?: string;
  }>
> => {
  // TODO: Implement actual receipt text parsing
  // This would use regex patterns and/or ML to:
  // - Identify item lines vs headers/footers
  // - Extract item names, quantities, and prices
  // - Detect discount/coupon lines
  // - Handle multi-line items

  logger.debug({ textLength: text.length }, 'Parsing receipt text');

  // Common receipt patterns to look for:
  // - "ITEM NAME          $X.XX"
  // - "2 @ $X.XX          $Y.YY"
  // - "DISCOUNT          -$X.XX"

  const items: Array<{
    description: string;
    quantity: number;
    priceCents: number;
    lineNumber: number;
    isDiscount: boolean;
    unitPriceCents?: number;
    unitType?: string;
  }> = [];

  // Simple line-by-line parsing as placeholder
  const lines = text.split('\n').filter((line) => line.trim());

  // Pattern for price at end of line: $X.XX or X.XX
  const pricePattern = /\$?(\d+)\.(\d{2})\s*$/;
  // Pattern for quantity: "2 @" or "2x" or just a number at start
  const quantityPattern = /^(\d+)\s*[@xX]/;

  let lineNumber = 0;
  for (const line of lines) {
    lineNumber++;
    const trimmedLine = line.trim();

    // Skip empty lines and common receipt headers/footers
    if (!trimmedLine || isHeaderOrFooter(trimmedLine)) {
      continue;
    }

    const priceMatch = trimmedLine.match(pricePattern);
    if (priceMatch) {
      const dollars = parseInt(priceMatch[1], 10);
      const cents = parseInt(priceMatch[2], 10);
      let priceCents = dollars * 100 + cents;

      // Check if it's a discount (negative)
      const isDiscount =
        trimmedLine.includes('-') || trimmedLine.toLowerCase().includes('discount');
      if (isDiscount) {
        priceCents = -Math.abs(priceCents);
      }

      // Extract description (everything before the price)
      let description = trimmedLine.replace(pricePattern, '').trim();
      let quantity = 1;

      // Check for quantity pattern
      const quantityMatch = description.match(quantityPattern);
      if (quantityMatch) {
        quantity = parseInt(quantityMatch[1], 10);
        description = description.replace(quantityPattern, '').trim();
      }

      // Clean up description
      description = description.replace(/[-]+\s*$/, '').trim();

      if (description) {
        items.push({
          description,
          quantity,
          priceCents,
          lineNumber,
          isDiscount,
        });
      }
    }
  }

  logger.debug({ extractedCount: items.length }, 'Receipt text parsing complete');
  return items;
};

/**
 * Checks if a line is likely a header or footer to skip.
 */
const isHeaderOrFooter = (line: string): boolean => {
  const lowercaseLine = line.toLowerCase();
  const skipPatterns = [
    'thank you',
    'thanks for',
    'visit us',
    'total',
    'subtotal',
    'tax',
    'change',
    'cash',
    'credit',
    'debit',
    'visa',
    'mastercard',
    'approved',
    'transaction',
    'terminal',
    'receipt',
    'store #',
    'date:',
    'time:',
    'cashier',
  ];

  return skipPatterns.some((pattern) => lowercaseLine.includes(pattern));
};

/**
 * Extracts metadata from receipt text (total, date, etc.).
 */
const extractReceiptMetadata = (
  text: string,
  logger: Logger,
): {
  totalCents?: number;
  transactionDate?: string;
} => {
  const result: { totalCents?: number; transactionDate?: string } = {};

  // Look for total amount
  const totalPatterns = [
    /total[:\s]+\$?(\d+)\.(\d{2})/i,
    /grand total[:\s]+\$?(\d+)\.(\d{2})/i,
    /amount due[:\s]+\$?(\d+)\.(\d{2})/i,
  ];

  for (const pattern of totalPatterns) {
    const match = text.match(pattern);
    if (match) {
      result.totalCents = parseInt(match[1], 10) * 100 + parseInt(match[2], 10);
      break;
    }
  }

  // Look for transaction date
  const datePatterns = [
    /(\d{1,2})\/(\d{1,2})\/(\d{2,4})/, // MM/DD/YYYY or M/D/YY
    /(\d{4})-(\d{2})-(\d{2})/, // YYYY-MM-DD
  ];

  for (const pattern of datePatterns) {
    const match = text.match(pattern);
    if (match) {
      // Try to parse the date
      try {
        let year: number;
        let month: number;
        let day: number;

        if (match[0].includes('-')) {
          // YYYY-MM-DD format
          year = parseInt(match[1], 10);
          month = parseInt(match[2], 10);
          day = parseInt(match[3], 10);
        } else {
          // MM/DD/YYYY format
          month = parseInt(match[1], 10);
          day = parseInt(match[2], 10);
          year = parseInt(match[3], 10);
          if (year < 100) {
            year += 2000;
          }
        }

        const date = new Date(year, month - 1, day);
        if (!isNaN(date.getTime())) {
          result.transactionDate = date.toISOString().split('T')[0];
          break;
        }
      } catch {
        // Continue to next pattern
      }
    }
  }

  logger.debug({ result }, 'Extracted receipt metadata');
  return result;
};

// ============================================================================
// RECEIPT ITEMS
// ============================================================================

/**
 * Gets all items for a receipt.
 * @param receiptId The receipt ID
 * @param logger Pino logger instance
 * @returns Array of receipt items
 */
export const getReceiptItems = async (
  receiptId: number,
  logger: Logger,
): Promise<ReceiptItem[]> => {
  return receiptRepo.getReceiptItems(receiptId, logger);
};

/**
 * Updates a receipt item (e.g., after manual matching).
 * @param receiptItemId The receipt item ID
 * @param updates Updates to apply
 * @param logger Pino logger instance
 * @returns Updated receipt item
 */
export const updateReceiptItem = async (
  receiptItemId: number,
  updates: UpdateReceiptItemRequest,
  logger: Logger,
): Promise<ReceiptItem> => {
  return receiptRepo.updateReceiptItem(receiptItemId, updates, logger);
};

/**
 * Gets receipt items that haven't been added to inventory.
 * @param receiptId The receipt ID
 * @param logger Pino logger instance
 * @returns Array of unadded items
 */
export const getUnaddedItems = async (
  receiptId: number,
  logger: Logger,
): Promise<ReceiptItem[]> => {
  return receiptRepo.getUnaddedReceiptItems(receiptId, logger);
};

// ============================================================================
// PROCESSING LOGS AND STATS
// ============================================================================

/**
 * Gets processing logs for a receipt.
 * @param receiptId The receipt ID
 * @param logger Pino logger instance
 * @returns Array of processing log records
 */
export const getProcessingLogs = async (
  receiptId: number,
  logger: Logger,
): Promise<ReceiptProcessingLogRecord[]> => {
  return receiptRepo.getProcessingLogs(receiptId, logger);
};

/**
 * Gets receipt processing statistics.
 * @param logger Pino logger instance
 * @param options Date range options
 * @returns Processing statistics
 */
export const getProcessingStats = async (
  logger: Logger,
  options: { fromDate?: string; toDate?: string } = {},
): Promise<{
  total_receipts: number;
  completed: number;
  failed: number;
  pending: number;
  avg_processing_time_ms: number;
  total_cost_cents: number;
}> => {
  return receiptRepo.getProcessingStats(logger, options);
};

/**
 * Gets receipts that need processing (for worker).
 * @param limit Maximum number of receipts to return
 * @param logger Pino logger instance
 * @returns Array of receipts needing processing
 */
export const getReceiptsNeedingProcessing = async (
  limit: number,
  logger: Logger,
): Promise<ReceiptScan[]> => {
  return receiptRepo.getReceiptsNeedingProcessing(MAX_RETRY_ATTEMPTS, limit, logger);
};

// ============================================================================
// STORE PATTERNS (Admin)
// ============================================================================

/**
 * Adds a new store receipt pattern for detection.
 * @param storeId The store ID
 * @param patternType The pattern type
 * @param patternValue The pattern value
 * @param logger Pino logger instance
 * @param options Additional options
 */
export const addStorePattern = async (
  storeId: number,
  patternType: string,
  patternValue: string,
  logger: Logger,
  options: { priority?: number } = {},
) => {
  return receiptRepo.addStorePattern(storeId, patternType, patternValue, logger, options);
};

/**
 * Gets all active store patterns.
 * @param logger Pino logger instance
 */
export const getActiveStorePatterns = async (logger: Logger) => {
  return receiptRepo.getActiveStorePatterns(logger);
};

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

import type { Job } from 'bullmq';
import type { ReceiptJobData } from '../types/job-data';
import { aiService } from './aiService.server';
import { isAiConfigured } from '../config/env';
import path from 'node:path';
import fs from 'node:fs/promises';

/**
 * Processes a receipt processing job from the queue.
 * This is the main entry point for background receipt processing.
 * @param job The BullMQ job
 * @param logger Pino logger instance
 * @returns Processing result
 */
export const processReceiptJob = async (
  job: Job<ReceiptJobData>,
  logger: Logger,
): Promise<{ success: boolean; itemsFound: number; receiptId: number }> => {
  const { receiptId, userId } = job.data;
  const jobLogger = logger.child({
    jobId: job.id,
    receiptId,
    userId,
    requestId: job.data.meta?.requestId,
  });

  jobLogger.info('Starting receipt processing job');

  try {
    // Get the receipt record to verify ownership and status
    const existingReceipt = await receiptRepo.getReceiptById(receiptId, userId, jobLogger);

    if (existingReceipt.status === 'completed') {
      jobLogger.info('Receipt already processed, skipping');
      return { success: true, itemsFound: 0, receiptId };
    }

    // Process the receipt (this handles status updates internally)
    const result = await processReceipt(receiptId, jobLogger);

    const itemsFound = result.items.length;
    const isSuccess = result.receipt.status === 'completed';

    jobLogger.info(
      { itemsFound, status: result.receipt.status },
      'Receipt processing job completed',
    );

    return {
      success: isSuccess,
      itemsFound,
      receiptId,
    };
  } catch (error) {
    jobLogger.error({ err: error }, 'Receipt processing job failed');

    // Update receipt status to failed
    try {
      await receiptRepo.updateReceipt(
        receiptId,
        {
          status: 'failed',
          error_details: {
            error: error instanceof Error ? error.message : String(error),
            jobId: job.id,
            attemptsMade: job.attemptsMade,
          },
        },
        jobLogger,
      );
    } catch (updateError) {
      jobLogger.error({ err: updateError }, 'Failed to update receipt status after error');
    }

    throw error;
  }
};
