// src/services/flyerProcessingService.server.ts
import type { Job, JobsOptions } from 'bullmq';
import path from 'path';
import type { Dirent } from 'node:fs';
import { z } from 'zod';

import type { AIService } from './aiService.server';
import type * as db from './db/index.db';
import { createFlyerAndItems } from './db/flyer.db';
import { PdfConversionError, AiDataValidationError } from './processingErrors';
import { FlyerDataTransformer } from './flyerDataTransformer';
import { logger as globalLogger } from './logger.server';
import type { Logger } from 'pino';


// Helper for consistent required string validation (handles missing/null/empty)
const requiredString = (message: string) => z.preprocess((val) => val ?? '', z.string().min(1, message));

// --- Start: Interfaces for Dependency Injection ---

export interface IFileSystem {
  readdir(path: string, options: { withFileTypes: true }): Promise<Dirent[]>;
  unlink(path: string): Promise<void>;
}

export interface ICommandExecutor {
  (command: string): Promise<{ stdout: string; stderr: string }>;
}

export interface FlyerJobData {
  filePath: string;
  originalFileName: string;
  checksum: string;
  userId?: string;
  submitterIp?: string;
  userProfileAddress?: string;
}

interface CleanupJobData {
  flyerId: number;
  // An array of absolute file paths to be deleted. Made optional for manual cleanup triggers.
  paths?: string[];
}

/**
 * Defines the contract for a queue that can have cleanup jobs added to it.
 * This is used for dependency injection to avoid circular dependencies.
 */
interface ICleanupQueue {
  add(name: string, data: CleanupJobData, opts?: JobsOptions): Promise<Job<CleanupJobData>>;
}

// --- Zod Schemas for AI Response Validation (exported for the transformer) ---
const ExtractedFlyerItemSchema = z.object({
  item: z.string(),
  price_display: z.string(),
  price_in_cents: z.number().nullable(),
  quantity: z.string(),
  category_name: z.string(),
  master_item_id: z.number().nullish(), // .nullish() allows null or undefined
});

export const AiFlyerDataSchema = z.object({
  store_name: requiredString( "Store name cannot be empty" ),
  valid_from: z.string().nullable(),
  valid_to: z.string().nullable(),
  store_address: z.string().nullable(),
  items: z.array(ExtractedFlyerItemSchema),
});

/**
 * This class encapsulates the business logic for processing a flyer from a file.
 * It handles PDF conversion, AI data extraction, and saving the results to the database.
 */
export class FlyerProcessingService {
  constructor(
    private ai: AIService,
    private database: typeof db,
    private fs: IFileSystem,
    private exec: ICommandExecutor,
    private cleanupQueue: ICleanupQueue,
    private transformer: FlyerDataTransformer,
  ) {}

  /**
   * Converts a PDF file to a series of JPEG images using an external tool.
   * @param filePath The path to the PDF file.
   * @param job The BullMQ job instance for progress updates.
   * @returns A promise that resolves to an array of paths to the created image files.
   */
  private async _convertPdfToImages(filePath: string, job: Job<FlyerJobData>, logger: Logger): Promise<string[]> {
    logger.info(`Starting PDF conversion for: ${filePath}`);
    await job.updateProgress({ message: 'Converting PDF to images...' });

    const outputDir = path.dirname(filePath);
    const outputFilePrefix = path.join(outputDir, path.basename(filePath, '.pdf'));
    logger.debug({ outputDir, outputFilePrefix }, `PDF output details`);

    const command = `pdftocairo -jpeg -r 150 "${filePath}" "${outputFilePrefix}"`;
    logger.info(`Executing PDF conversion command`);
    logger.debug({ command });
    const { stdout, stderr } = await this.exec(command);

    if (stdout) logger.debug({ stdout }, `[Worker] pdftocairo stdout for ${filePath}:`);
    if (stderr) logger.warn({ stderr }, `[Worker] pdftocairo stderr for ${filePath}:`);

    logger.debug(`[Worker] Reading contents of output directory: ${outputDir}`);
    const filesInDir = await this.fs.readdir(outputDir, { withFileTypes: true });
    logger.debug(`[Worker] Found ${filesInDir.length} total entries in output directory.`);

    const generatedImages = filesInDir
      .filter(f => f.name.startsWith(path.basename(outputFilePrefix)) && f.name.endsWith('.jpg'))
      .sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));

    logger.debug({ imageNames: generatedImages.map(f => f.name) }, `Filtered down to ${generatedImages.length} generated JPGs.`);

    if (generatedImages.length === 0) {
      const errorMessage = `PDF conversion resulted in 0 images for file: ${filePath}. The PDF might be blank or corrupt.`;
      logger.error({ stderr }, `PdfConversionError: ${errorMessage}`);
      throw new PdfConversionError(errorMessage, stderr);
    }

    return generatedImages.map(img => path.join(outputDir, img.name));
  }

  /**
   * Prepares the input images for the AI service. If the input is a PDF, it's converted to images.
   * @param filePath The path to the original uploaded file.
   * @param job The BullMQ job instance.
   * @returns An object containing the final image paths for the AI and a list of any newly created image files.
   */
  private async _prepareImageInputs(filePath: string, job: Job<FlyerJobData>, logger: Logger): Promise<{ imagePaths: { path: string; mimetype: string }[], createdImagePaths: string[] }> {
    const fileExt = path.extname(filePath).toLowerCase();

    if (fileExt === '.pdf') {
      const createdImagePaths = await this._convertPdfToImages(filePath, job, logger);
      const imagePaths = createdImagePaths.map(p => ({ path: p, mimetype: 'image/jpeg' }));
      logger.info(`Converted PDF to ${imagePaths.length} images.`);
      return { imagePaths, createdImagePaths };
    } else {
      logger.info(`Processing as a single image file: ${filePath}`);
      const imagePaths = [{ path: filePath, mimetype: `image/${fileExt.slice(1)}` }];
      return { imagePaths, createdImagePaths: [] };
    }
  }

  /**
   * Calls the AI service to extract structured data from the flyer images.
   * @param imagePaths An array of paths and mimetypes for the images.
   * @param jobData The data from the BullMQ job.
   * @returns A promise that resolves to the validated, structured flyer data.
   */
  private async _extractFlyerDataWithAI(imagePaths: { path: string; mimetype: string }[], jobData: FlyerJobData, logger: Logger): Promise<z.infer<typeof AiFlyerDataSchema>> {
    logger.info(`Starting AI data extraction.`);
    const { submitterIp, userProfileAddress } = jobData;
    const masterItems = await this.database.personalizationRepo.getAllMasterItems(logger);
    logger.debug(`Retrieved ${masterItems.length} master items for AI matching.`);

    const extractedData = await this.ai.extractCoreDataFromFlyerImage(
      imagePaths,
      masterItems,
        submitterIp, // Pass the job-specific logger
        userProfileAddress, // Pass the job-specific logger
      logger
    );

    const validationResult = AiFlyerDataSchema.safeParse(extractedData);
    if (!validationResult.success) {
      const errors = validationResult.error.flatten(); logger.error({ errors, rawData: extractedData }, 'AI response failed validation.');
      throw new AiDataValidationError('AI response validation failed. The returned data structure is incorrect.', errors, extractedData);
    }

    logger.info(`AI extracted ${validationResult.data.items.length} items.`);
    return validationResult.data;
  }

  /**
   * Saves the extracted flyer data to the database.
   * @param extractedData The structured data from the AI.
   * @param imagePaths The paths to the flyer images.
   * @param jobData The data from the BullMQ job.
   * @returns A promise that resolves to the newly created flyer record.
   */
  private async _saveProcessedFlyerData(
    extractedData: z.infer<typeof AiFlyerDataSchema>,
    imagePaths: { path: string; mimetype: string }[],
    jobData: FlyerJobData,
    logger: Logger
  ) {
    logger.info(`Preparing to save extracted data to database.`);

    // 1. Transform the AI data into database-ready records.
    const { flyerData, itemsForDb } = await this.transformer.transform(
      extractedData,
      imagePaths,
      jobData.originalFileName,
      jobData.checksum,
      jobData.userId,
      // Pass the job-specific logger to the transformer
      logger 
    );

    // 2. Save the transformed data to the database.
    const { flyer: newFlyer } = await createFlyerAndItems(flyerData, itemsForDb, logger);
    logger.info({ newFlyerId: newFlyer.flyer_id }, `Successfully saved new flyer.`);

    await this.database.adminRepo.logActivity({ userId: jobData.userId, action: 'flyer_processed', displayText: `Processed a new flyer for ${flyerData.store_name}.`, details: { flyerId: newFlyer.flyer_id, storeName: flyerData.store_name } }, logger);

    return newFlyer;
  }

  /**
   * Enqueues a job to clean up temporary files associated with a flyer upload.
   * @param flyerId The ID of the processed flyer.
   * @param paths An array of file paths to be deleted.
   */
  private async _enqueueCleanup(flyerId: number, paths: string[], logger: Logger): Promise<void> {
    if (paths.length === 0) return;

    await this.cleanupQueue.add('cleanup-flyer-files', { flyerId, paths }, {
      jobId: `cleanup-flyer-${flyerId}`,
      removeOnComplete: true,
    });
    logger.info({ flyerId }, `Enqueued cleanup job.`);
  }


  async processJob(job: Job<FlyerJobData>) {
    const { filePath, originalFileName } = job.data;
    const createdImagePaths: string[] = [];
    let newFlyerId: number | undefined;

    // Create a job-specific logger instance with context, as per ADR-004
    const logger = globalLogger.child({
      jobId: job.id,
      jobName: job.name,
      userId: job.data.userId,
      checksum: job.data.checksum,
      originalFileName,
    });

    logger.info(`Picked up job.`);

    try {
      await job.updateProgress({ message: 'Starting process...' });
      const { imagePaths, createdImagePaths: tempImagePaths } = await this._prepareImageInputs(filePath, job, logger);
      createdImagePaths.push(...tempImagePaths);

      await job.updateProgress({ message: 'Extracting data...' });
      const extractedData = await this._extractFlyerDataWithAI(imagePaths, job.data, logger);

      await job.updateProgress({ message: 'Saving to database...' });
      const newFlyer = await this._saveProcessedFlyerData(extractedData, imagePaths, job.data, logger); // Pass logger

      newFlyerId = newFlyer.flyer_id;
      logger.info({ flyerId: newFlyerId }, `Job processed successfully.`);
      return { flyerId: newFlyer.flyer_id };
    } catch (error: unknown) {
      let errorMessage = 'An unknown error occurred';
      if (error instanceof PdfConversionError) {
        errorMessage = error.message;
        logger.error({ err: error, stderr: error.stderr }, `PDF Conversion failed.`);
      } else if (error instanceof AiDataValidationError) {
        errorMessage = error.message;
        logger.error({ err: error, validationErrors: error.validationErrors, rawData: error.rawData }, `AI Data Validation failed.`);
      } else if (error instanceof Error) {
        errorMessage = error.message;
        logger.error({ err: error, attemptsMade: job.attemptsMade, totalAttempts: job.opts.attempts }, `A generic error occurred in job.`);
      }
      await job.updateProgress({ message: `Error: ${errorMessage}` });
      throw error;
    } finally {
      if (newFlyerId) {
        const pathsToClean = [filePath, ...createdImagePaths];
        await this._enqueueCleanup(newFlyerId, pathsToClean, logger);
      } else {
        logger.warn(`Job failed. Temporary files will NOT be cleaned up to allow for manual inspection.`);
      }
    }
  }
}