// src/services/barcodeService.server.ts
/**
 * @file Barcode Detection Service
 * Provides barcode/UPC detection from images using zxing-wasm.
 * Supports UPC-A, UPC-E, EAN-13, EAN-8, CODE-128, CODE-39, and QR codes.
 */
import type { Logger } from 'pino';
import type { Job } from 'bullmq';
import type { BarcodeDetectionJobData } from '../types/job-data';
import type { BarcodeDetectionResult } from '../types/upc';
import { upcRepo } from './db/index.db';
import sharp from 'sharp';
import fs from 'node:fs/promises';

/**
 * Supported barcode formats for detection.
 */
export type BarcodeFormat =
  | 'UPC-A'
  | 'UPC-E'
  | 'EAN-13'
  | 'EAN-8'
  | 'CODE-128'
  | 'CODE-39'
  | 'QR_CODE'
  | 'unknown';

/**
 * Maps zxing-wasm format names to our BarcodeFormat type.
 */
const formatMap: Record<string, BarcodeFormat> = {
  'UPC-A': 'UPC-A',
  'UPC-E': 'UPC-E',
  'EAN-13': 'EAN-13',
  'EAN-8': 'EAN-8',
  Code128: 'CODE-128',
  Code39: 'CODE-39',
  QRCode: 'QR_CODE',
};

/**
 * Detects barcodes in an image using zxing-wasm.
 *
 * @param imagePath Path to the image file
 * @param logger Pino logger instance
 * @returns Detection result with UPC code if found
 */
export const detectBarcode = async (
  imagePath: string,
  logger: Logger,
): Promise<BarcodeDetectionResult> => {
  const detectionLogger = logger.child({ imagePath });
  detectionLogger.info('Starting barcode detection');

  try {
    // Dynamically import zxing-wasm (ES module)
    const { readBarcodesFromImageData } = await import('zxing-wasm/reader');

    // Read and process the image with sharp
    const imageBuffer = await fs.readFile(imagePath);

    // Convert to raw pixel data (RGBA)
    const image = sharp(imageBuffer);
    const metadata = await image.metadata();

    if (!metadata.width || !metadata.height) {
      detectionLogger.warn('Could not determine image dimensions');
      return {
        detected: false,
        upc_code: null,
        confidence: null,
        format: null,
        error: 'Could not determine image dimensions',
      };
    }

    // Convert to raw RGBA pixels
    const { data, info } = await image.ensureAlpha().raw().toBuffer({ resolveWithObject: true });

    // Create ImageData-like object for zxing-wasm
    const imageData = {
      data: new Uint8ClampedArray(data),
      width: info.width,
      height: info.height,
      colorSpace: 'srgb' as const,
    };

    detectionLogger.debug(
      { width: info.width, height: info.height },
      'Processing image for barcode detection',
    );

    // Attempt barcode detection
    const results = await readBarcodesFromImageData(imageData as ImageData, {
      tryHarder: true,
      tryRotate: true,
      tryInvert: true,
      formats: ['UPC-A', 'UPC-E', 'EAN-13', 'EAN-8', 'Code128', 'Code39'],
    });

    if (results.length === 0) {
      detectionLogger.info('No barcode detected in image');
      return {
        detected: false,
        upc_code: null,
        confidence: null,
        format: null,
        error: null,
      };
    }

    // Take the first (best) result
    const bestResult = results[0];
    const format = formatMap[bestResult.format] || 'unknown';

    // Calculate confidence based on result quality indicators
    // zxing-wasm doesn't provide direct confidence, so we estimate based on format match
    const confidence = bestResult.text ? 0.95 : 0.5;

    detectionLogger.info(
      { upcCode: bestResult.text, format, confidence },
      'Barcode detected successfully',
    );

    return {
      detected: true,
      upc_code: bestResult.text,
      confidence,
      format,
      error: null,
    };
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    detectionLogger.error({ err: error }, 'Barcode detection failed');

    return {
      detected: false,
      upc_code: null,
      confidence: null,
      format: null,
      error: errorMessage,
    };
  }
};

/**
 * Validates a UPC code format.
 * @param code The code to validate
 * @returns True if valid UPC format
 */
export const isValidUpcFormat = (code: string): boolean => {
  // UPC-A: 12 digits
  // UPC-E: 8 digits
  // EAN-13: 13 digits
  // EAN-8: 8 digits
  return /^[0-9]{8,14}$/.test(code);
};

/**
 * Calculates the check digit for a UPC-A code.
 * @param code The 11-digit UPC-A code (without check digit)
 * @returns The check digit
 */
export const calculateUpcCheckDigit = (code: string): number | null => {
  if (code.length !== 11 || !/^\d+$/.test(code)) {
    return null;
  }

  let sum = 0;
  for (let i = 0; i < 11; i++) {
    const digit = parseInt(code[i], 10);
    // Odd positions (0, 2, 4, ...) multiplied by 3
    // Even positions (1, 3, 5, ...) multiplied by 1
    sum += digit * (i % 2 === 0 ? 3 : 1);
  }

  const checkDigit = (10 - (sum % 10)) % 10;
  return checkDigit;
};

/**
 * Validates a UPC code including check digit.
 * @param code The complete UPC code
 * @returns True if check digit is valid
 */
export const validateUpcCheckDigit = (code: string): boolean => {
  if (code.length !== 12 || !/^\d+$/.test(code)) {
    return false;
  }

  const codeWithoutCheck = code.slice(0, 11);
  const providedCheck = parseInt(code[11], 10);
  const calculatedCheck = calculateUpcCheckDigit(codeWithoutCheck);

  return calculatedCheck === providedCheck;
};

/**
 * Processes a barcode detection job from the queue.
 * @param job The BullMQ job
 * @param logger Pino logger instance
 * @returns Detection result
 */
export const processBarcodeDetectionJob = async (
  job: Job<BarcodeDetectionJobData>,
  logger: Logger,
): Promise<BarcodeDetectionResult> => {
  const { scanId, imagePath, userId } = job.data;
  const jobLogger = logger.child({
    jobId: job.id,
    scanId,
    userId,
    requestId: job.data.meta?.requestId,
  });

  jobLogger.info('Processing barcode detection job');

  try {
    // Attempt barcode detection
    const result = await detectBarcode(imagePath, jobLogger);

    // If a code was detected, update the scan record
    if (result.detected && result.upc_code) {
      await upcRepo.updateScanWithDetectedCode(
        scanId,
        result.upc_code,
        result.confidence,
        jobLogger,
      );

      jobLogger.info(
        { upcCode: result.upc_code, confidence: result.confidence },
        'Barcode detected and scan record updated',
      );
    } else {
      jobLogger.info('No barcode detected in image');
    }

    return result;
  } catch (error) {
    jobLogger.error({ err: error }, 'Barcode detection job failed');

    return {
      detected: false,
      upc_code: null,
      confidence: null,
      format: null,
      error: error instanceof Error ? error.message : String(error),
    };
  }
};

/**
 * Detects multiple barcodes in an image.
 * Useful for receipts or product lists with multiple items.
 * @param imagePath Path to the image file
 * @param logger Pino logger instance
 * @returns Array of detection results
 */
export const detectMultipleBarcodes = async (
  imagePath: string,
  logger: Logger,
): Promise<BarcodeDetectionResult[]> => {
  const detectionLogger = logger.child({ imagePath });
  detectionLogger.info('Starting multiple barcode detection');

  try {
    const { readBarcodesFromImageData } = await import('zxing-wasm/reader');

    // Read and process the image
    const imageBuffer = await fs.readFile(imagePath);
    const image = sharp(imageBuffer);

    const { data, info } = await image.ensureAlpha().raw().toBuffer({ resolveWithObject: true });

    const imageData = {
      data: new Uint8ClampedArray(data),
      width: info.width,
      height: info.height,
      colorSpace: 'srgb' as const,
    };

    // Detect all barcodes
    const results = await readBarcodesFromImageData(imageData as ImageData, {
      tryHarder: true,
      tryRotate: true,
      tryInvert: true,
      formats: ['UPC-A', 'UPC-E', 'EAN-13', 'EAN-8', 'Code128', 'Code39'],
    });

    detectionLogger.info({ count: results.length }, 'Multiple barcode detection complete');

    return results.map((result) => ({
      detected: true,
      upc_code: result.text,
      confidence: 0.95,
      format: formatMap[result.format] || 'unknown',
      error: null,
    }));
  } catch (error) {
    detectionLogger.error({ err: error }, 'Multiple barcode detection failed');
    return [];
  }
};

/**
 * Enhances image for better barcode detection.
 * Applies preprocessing like grayscale conversion, contrast adjustment, etc.
 * @param imagePath Path to the source image
 * @param logger Pino logger instance
 * @returns Path to enhanced image (or original if enhancement fails)
 */
export const enhanceImageForDetection = async (
  imagePath: string,
  logger: Logger,
): Promise<string> => {
  const detectionLogger = logger.child({ imagePath });

  try {
    // Create enhanced version with improved contrast for barcode detection
    const enhancedPath = imagePath.replace(/(\.[^.]+)$/, '-enhanced$1');

    await sharp(imagePath)
      .grayscale()
      .normalize() // Improve contrast
      .sharpen() // Enhance edges
      .toFile(enhancedPath);

    detectionLogger.debug({ enhancedPath }, 'Image enhanced for barcode detection');
    return enhancedPath;
  } catch (error) {
    detectionLogger.warn({ err: error }, 'Image enhancement failed, using original');
    return imagePath;
  }
};
