// src/services/upcService.server.ts
/**
 * @file UPC Scanning Service
 * Handles UPC barcode scanning, lookup, and external API integration.
 * Provides functionality for scanning barcodes from images and manual entry.
 */
import type { Logger } from 'pino';
import { upcRepo } from './db/index.db';
import type {
  UpcScanRequest,
  UpcScanResult,
  UpcLookupResult,
  UpcProductMatch,
  UpcExternalProductInfo,
  UpcExternalLookupOptions,
  UpcScanHistoryQueryOptions,
  UpcScanHistoryRecord,
  BarcodeDetectionResult,
} from '../types/upc';
import { config, isUpcItemDbConfigured, isBarcodeLookupConfigured } from '../config/env';

/**
 * Default cache age for external lookups (7 days in hours)
 */
const DEFAULT_CACHE_AGE_HOURS = 168;

/**
 * UPC code validation regex (8-14 digits)
 */
const UPC_CODE_REGEX = /^[0-9]{8,14}$/;

/**
 * Validates a UPC code format.
 * @param upcCode The UPC code to validate
 * @returns True if the UPC code is valid, false otherwise
 */
export const isValidUpcCode = (upcCode: string): boolean => {
  return UPC_CODE_REGEX.test(upcCode);
};

/**
 * Normalizes a UPC code by removing spaces and dashes.
 * @param upcCode The raw UPC code input
 * @returns Normalized UPC code
 */
export const normalizeUpcCode = (upcCode: string): string => {
  return upcCode.replace(/[\s-]/g, '');
};

/**
 * Detects and decodes a barcode from an image.
 * This is a placeholder for actual barcode detection implementation.
 * In production, this would use a library like zxing-js, quagga, or an external service.
 * @param imageBase64 Base64-encoded image data
 * @param logger Pino logger instance
 * @returns Barcode detection result
 */
export const detectBarcodeFromImage = async (
  imageBase64: string,
  logger: Logger,
): Promise<BarcodeDetectionResult> => {
  logger.debug({ imageLength: imageBase64.length }, 'Attempting to detect barcode from image');

  // TODO: Implement actual barcode detection using a library like:
  // - @nickvdyck/barcode-reader (pure JS)
  // - dynamsoft-javascript-barcode (commercial)
  // - External service like Google Cloud Vision API
  //
  // For now, return a placeholder response indicating detection is not yet implemented
  logger.warn('Barcode detection from images is not yet implemented');

  return {
    detected: false,
    upc_code: null,
    confidence: null,
    format: null,
    error: 'Barcode detection from images is not yet implemented. Please use manual entry.',
  };
};

/**
 * Looks up product in Open Food Facts API (free, open source).
 * @param upcCode The UPC code to look up
 * @param logger Pino logger instance
 * @returns External product information or null if not found
 */
const lookupOpenFoodFacts = async (
  upcCode: string,
  logger: Logger,
): Promise<UpcExternalProductInfo | null> => {
  try {
    const openFoodFactsUrl = `https://world.openfoodfacts.org/api/v2/product/${upcCode}`;
    logger.debug({ url: openFoodFactsUrl }, 'Querying Open Food Facts API');

    const response = await fetch(openFoodFactsUrl, {
      headers: {
        'User-Agent': 'FlyerCrawler/1.0 (contact@projectium.com)',
      },
    });

    if (response.ok) {
      const data = await response.json();

      if (data.status === 1 && data.product) {
        const product = data.product;
        logger.info(
          { upcCode, productName: product.product_name },
          'Found product in Open Food Facts',
        );

        return {
          name: product.product_name || product.generic_name || 'Unknown Product',
          brand: product.brands || null,
          category: product.categories_tags?.[0]?.replace('en:', '') || null,
          description: product.ingredients_text || null,
          image_url: product.image_url || product.image_front_url || null,
          source: 'openfoodfacts',
          raw_data: product,
        };
      }
    }

    logger.debug({ upcCode }, 'Product not found in Open Food Facts');
  } catch (error) {
    const err = error instanceof Error ? error : new Error(String(error));
    logger.warn({ err, upcCode }, 'Error querying Open Food Facts API');
  }

  return null;
};

/**
 * Looks up product in UPC Item DB API.
 * Requires UPC_ITEM_DB_API_KEY environment variable.
 * @see https://www.upcitemdb.com/wp/docs/main/development/
 * @param upcCode The UPC code to look up
 * @param logger Pino logger instance
 * @returns External product information or null if not found
 */
const lookupUpcItemDb = async (
  upcCode: string,
  logger: Logger,
): Promise<UpcExternalProductInfo | null> => {
  if (!isUpcItemDbConfigured) {
    logger.debug('UPC Item DB API key not configured, skipping');
    return null;
  }

  try {
    const url = `https://api.upcitemdb.com/prod/trial/lookup?upc=${upcCode}`;
    logger.debug({ url }, 'Querying UPC Item DB API');

    const response = await fetch(url, {
      headers: {
        'Content-Type': 'application/json',
        user_key: config.upc.upcItemDbApiKey!,
        key_type: '3scale',
      },
    });

    if (response.ok) {
      const data = await response.json();

      if (data.code === 'OK' && data.items && data.items.length > 0) {
        const item = data.items[0];
        logger.info({ upcCode, productName: item.title }, 'Found product in UPC Item DB');

        return {
          name: item.title || 'Unknown Product',
          brand: item.brand || null,
          category: item.category || null,
          description: item.description || null,
          image_url: item.images?.[0] || null,
          source: 'upcitemdb',
          raw_data: item,
        };
      }
    } else if (response.status === 429) {
      logger.warn({ upcCode }, 'UPC Item DB rate limit exceeded');
    }

    logger.debug({ upcCode }, 'Product not found in UPC Item DB');
  } catch (error) {
    const err = error instanceof Error ? error : new Error(String(error));
    logger.warn({ err, upcCode }, 'Error querying UPC Item DB API');
  }

  return null;
};

/**
 * Looks up product in Barcode Lookup API.
 * Requires BARCODE_LOOKUP_API_KEY environment variable.
 * @see https://www.barcodelookup.com/api
 * @param upcCode The UPC code to look up
 * @param logger Pino logger instance
 * @returns External product information or null if not found
 */
const lookupBarcodeLookup = async (
  upcCode: string,
  logger: Logger,
): Promise<UpcExternalProductInfo | null> => {
  if (!isBarcodeLookupConfigured) {
    logger.debug('Barcode Lookup API key not configured, skipping');
    return null;
  }

  try {
    const url = `https://api.barcodelookup.com/v3/products?barcode=${upcCode}&key=${config.upc.barcodeLookupApiKey}`;
    logger.debug('Querying Barcode Lookup API');

    const response = await fetch(url, {
      headers: {
        Accept: 'application/json',
      },
    });

    if (response.ok) {
      const data = await response.json();

      if (data.products && data.products.length > 0) {
        const product = data.products[0];
        logger.info({ upcCode, productName: product.title }, 'Found product in Barcode Lookup');

        return {
          name: product.title || product.product_name || 'Unknown Product',
          brand: product.brand || null,
          category: product.category || null,
          description: product.description || null,
          image_url: product.images?.[0] || null,
          source: 'barcodelookup',
          raw_data: product,
        };
      }
    } else if (response.status === 429) {
      logger.warn({ upcCode }, 'Barcode Lookup rate limit exceeded');
    } else if (response.status === 404) {
      logger.debug({ upcCode }, 'Product not found in Barcode Lookup');
    }
  } catch (error) {
    const err = error instanceof Error ? error : new Error(String(error));
    logger.warn({ err, upcCode }, 'Error querying Barcode Lookup API');
  }

  return null;
};

/**
 * Looks up product information from external UPC databases.
 * Tries multiple APIs in order of preference:
 * 1. Open Food Facts (free, open source)
 * 2. UPC Item DB (requires API key)
 * 3. Barcode Lookup (requires API key)
 * @param upcCode The UPC code to look up
 * @param logger Pino logger instance
 * @returns External product information or null if not found
 */
export const lookupExternalUpc = async (
  upcCode: string,
  logger: Logger,
): Promise<UpcExternalProductInfo | null> => {
  logger.debug({ upcCode }, 'Looking up UPC in external databases');

  // Try Open Food Facts first (free, no API key needed)
  let result = await lookupOpenFoodFacts(upcCode, logger);
  if (result) {
    return result;
  }

  // Try UPC Item DB if configured
  result = await lookupUpcItemDb(upcCode, logger);
  if (result) {
    return result;
  }

  // Try Barcode Lookup if configured
  result = await lookupBarcodeLookup(upcCode, logger);
  if (result) {
    return result;
  }

  logger.debug({ upcCode }, 'No external product information found');
  return null;
};

/**
 * Performs a UPC scan operation including barcode detection, database lookup,
 * and optional external API lookup.
 * @param userId The user performing the scan
 * @param request The scan request containing UPC code or image
 * @param logger Pino logger instance
 * @returns Complete scan result with product information
 */
export const scanUpc = async (
  userId: string,
  request: UpcScanRequest,
  logger: Logger,
): Promise<UpcScanResult> => {
  const scanLogger = logger.child({ userId, scanSource: request.scan_source });
  scanLogger.info('Starting UPC scan');

  let upcCode: string | null = null;
  let scanConfidence: number | null = null;

  // Step 1: Get UPC code from request (manual entry or image detection)
  if (request.upc_code) {
    // Manual entry - normalize and validate
    upcCode = normalizeUpcCode(request.upc_code);

    if (!isValidUpcCode(upcCode)) {
      scanLogger.warn({ upcCode }, 'Invalid UPC code format');
      throw new Error('Invalid UPC code format. UPC codes must be 8-14 digits.');
    }

    scanConfidence = 1.0; // Manual entry has 100% confidence
    scanLogger.debug({ upcCode }, 'Using manually entered UPC code');
  } else if (request.image_base64) {
    // Image detection
    const detection = await detectBarcodeFromImage(request.image_base64, scanLogger);

    if (!detection.detected || !detection.upc_code) {
      // Record the failed scan attempt
      const scanRecord = await upcRepo.recordScan(
        userId,
        'DETECTION_FAILED',
        request.scan_source,
        scanLogger,
        {
          scanConfidence: 0,
          lookupSuccessful: false,
        },
      );

      return {
        scan_id: scanRecord.scan_id,
        upc_code: '',
        product: null,
        external_lookup: null,
        confidence: 0,
        lookup_successful: false,
        is_new_product: false,
        scanned_at: scanRecord.created_at,
      };
    }

    upcCode = detection.upc_code;
    scanConfidence = detection.confidence;
    scanLogger.info({ upcCode, confidence: scanConfidence }, 'Barcode detected from image');
  } else {
    throw new Error('Either upc_code or image_base64 must be provided.');
  }

  // Step 2: Look up product in our database
  let product: UpcProductMatch | null = null;
  product = await upcRepo.findProductByUpc(upcCode, scanLogger);

  const isNewProduct = !product;
  scanLogger.debug({ upcCode, found: !!product, isNewProduct }, 'Local database lookup complete');

  // Step 3: If not found locally, check external APIs
  let externalLookup: UpcExternalProductInfo | null = null;

  if (!product) {
    // Check cache first
    const cachedLookup = await upcRepo.findExternalLookup(
      upcCode,
      DEFAULT_CACHE_AGE_HOURS,
      scanLogger,
    );

    if (cachedLookup) {
      scanLogger.debug({ upcCode }, 'Using cached external lookup');

      if (cachedLookup.lookup_successful) {
        externalLookup = {
          name: cachedLookup.product_name || 'Unknown Product',
          brand: cachedLookup.brand_name,
          category: cachedLookup.category,
          description: cachedLookup.description,
          image_url: cachedLookup.image_url,
          source: cachedLookup.external_source,
          raw_data: cachedLookup.lookup_data ?? undefined,
        };
      }
    } else {
      // Perform fresh external lookup
      externalLookup = await lookupExternalUpc(upcCode, scanLogger);

      // Cache the result (success or failure)
      await upcRepo.upsertExternalLookup(
        upcCode,
        externalLookup?.source || 'unknown',
        !!externalLookup,
        scanLogger,
        externalLookup
          ? {
              productName: externalLookup.name,
              brandName: externalLookup.brand,
              category: externalLookup.category,
              description: externalLookup.description,
              imageUrl: externalLookup.image_url,
              lookupData: externalLookup.raw_data as Record<string, unknown> | undefined,
            }
          : {},
      );
    }
  }

  // Step 4: Record the scan in history
  const lookupSuccessful = !!(product || externalLookup);
  const scanRecord = await upcRepo.recordScan(userId, upcCode, request.scan_source, scanLogger, {
    productId: product?.product_id,
    scanConfidence,
    lookupSuccessful,
  });

  scanLogger.info(
    { scanId: scanRecord.scan_id, upcCode, lookupSuccessful, isNewProduct },
    'UPC scan completed',
  );

  return {
    scan_id: scanRecord.scan_id,
    upc_code: upcCode,
    product,
    external_lookup: externalLookup,
    confidence: scanConfidence,
    lookup_successful: lookupSuccessful,
    is_new_product: isNewProduct,
    scanned_at: scanRecord.created_at,
  };
};

/**
 * Looks up a UPC code without recording scan history.
 * Useful for quick lookups or verification.
 * @param options Lookup options
 * @param logger Pino logger instance
 * @returns Lookup result with product information
 */
export const lookupUpc = async (
  options: UpcExternalLookupOptions,
  logger: Logger,
): Promise<UpcLookupResult> => {
  const {
    upc_code,
    force_refresh = false,
    max_cache_age_hours = DEFAULT_CACHE_AGE_HOURS,
  } = options;
  const lookupLogger = logger.child({ upcCode: upc_code });

  lookupLogger.debug('Performing UPC lookup');

  const normalizedUpc = normalizeUpcCode(upc_code);

  if (!isValidUpcCode(normalizedUpc)) {
    throw new Error('Invalid UPC code format. UPC codes must be 8-14 digits.');
  }

  // Check local database
  const product = await upcRepo.findProductByUpc(normalizedUpc, lookupLogger);

  if (product) {
    lookupLogger.debug({ productId: product.product_id }, 'Found product in local database');
    return {
      upc_code: normalizedUpc,
      product,
      external_lookup: null,
      found: true,
      from_cache: false,
    };
  }

  // Check external cache (unless force refresh)
  if (!force_refresh) {
    const cachedLookup = await upcRepo.findExternalLookup(
      normalizedUpc,
      max_cache_age_hours,
      lookupLogger,
    );

    if (cachedLookup) {
      lookupLogger.debug('Returning cached external lookup');

      if (cachedLookup.lookup_successful) {
        return {
          upc_code: normalizedUpc,
          product: null,
          external_lookup: {
            name: cachedLookup.product_name || 'Unknown Product',
            brand: cachedLookup.brand_name,
            category: cachedLookup.category,
            description: cachedLookup.description,
            image_url: cachedLookup.image_url,
            source: cachedLookup.external_source,
            raw_data: cachedLookup.lookup_data ?? undefined,
          },
          found: true,
          from_cache: true,
        };
      }

      // Cached lookup was unsuccessful
      return {
        upc_code: normalizedUpc,
        product: null,
        external_lookup: null,
        found: false,
        from_cache: true,
      };
    }
  }

  // Perform fresh external lookup
  const externalLookup = await lookupExternalUpc(normalizedUpc, lookupLogger);

  // Cache the result
  await upcRepo.upsertExternalLookup(
    normalizedUpc,
    externalLookup?.source || 'unknown',
    !!externalLookup,
    lookupLogger,
    externalLookup
      ? {
          productName: externalLookup.name,
          brandName: externalLookup.brand,
          category: externalLookup.category,
          description: externalLookup.description,
          imageUrl: externalLookup.image_url,
          lookupData: externalLookup.raw_data as Record<string, unknown> | undefined,
        }
      : {},
  );

  return {
    upc_code: normalizedUpc,
    product: null,
    external_lookup: externalLookup,
    found: !!externalLookup,
    from_cache: false,
  };
};

/**
 * Links a UPC code to an existing product (admin operation).
 * @param productId The product ID to link
 * @param upcCode The UPC code to link
 * @param logger Pino logger instance
 */
export const linkUpcToProduct = async (
  productId: number,
  upcCode: string,
  logger: Logger,
): Promise<void> => {
  const normalizedUpc = normalizeUpcCode(upcCode);

  if (!isValidUpcCode(normalizedUpc)) {
    throw new Error('Invalid UPC code format. UPC codes must be 8-14 digits.');
  }

  logger.info({ productId, upcCode: normalizedUpc }, 'Linking UPC code to product');
  await upcRepo.linkUpcToProduct(productId, normalizedUpc, logger);
  logger.info({ productId, upcCode: normalizedUpc }, 'UPC code linked successfully');
};

/**
 * Gets the scan history for a user.
 * @param options Query options
 * @param logger Pino logger instance
 * @returns Paginated scan history
 */
export const getScanHistory = async (
  options: UpcScanHistoryQueryOptions,
  logger: Logger,
): Promise<{ scans: UpcScanHistoryRecord[]; total: number }> => {
  logger.debug({ userId: options.user_id }, 'Fetching scan history');
  return upcRepo.getScanHistory(options, logger);
};

/**
 * Gets scan statistics for a user.
 * @param userId The user ID
 * @param logger Pino logger instance
 * @returns Scan statistics
 */
export const getScanStats = async (
  userId: string,
  logger: Logger,
): Promise<{
  total_scans: number;
  successful_lookups: number;
  unique_products: number;
  scans_today: number;
  scans_this_week: number;
}> => {
  logger.debug({ userId }, 'Fetching scan statistics');
  return upcRepo.getUserScanStats(userId, logger);
};

/**
 * Gets a single scan record by ID.
 * @param scanId The scan ID
 * @param userId The user ID (for authorization)
 * @param logger Pino logger instance
 * @returns The scan record
 */
export const getScanById = async (
  scanId: number,
  userId: string,
  logger: Logger,
): Promise<UpcScanHistoryRecord> => {
  logger.debug({ scanId, userId }, 'Fetching scan by ID');
  return upcRepo.getScanById(scanId, userId, logger);
};
