// src/services/flyerFileHandler.server.ts
import path from 'path';
import sharp from 'sharp';
import type { Dirent } from 'node:fs';
import type { Job } from 'bullmq';
import type { Logger } from 'pino';
import { ImageConversionError, PdfConversionError, UnsupportedFileTypeError } from './processingErrors';
import type { FlyerJobData } from '../types/job-data';
// Define the image formats supported by the AI model
const SUPPORTED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.webp', '.heic', '.heif'];
// Define image formats that are not directly supported but can be converted to PNG.
const CONVERTIBLE_IMAGE_EXTENSIONS = ['.gif', '.tiff', '.svg', '.bmp'];

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

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

/**
 * This class encapsulates the logic for handling different file types (PDF, images)
 * and preparing them for AI processing.
 */
export class FlyerFileHandler {
  constructor(
    private fs: IFileSystem,
    private exec: ICommandExecutor,
  ) {}

  /**
   * Executes the pdftocairo command to convert the PDF.
   */
  private async _executePdfConversion(
    filePath: string,
    outputFilePrefix: string,
    logger: Logger,
  ): Promise<{ stdout: string; stderr: string }> {
    const command = `pdftocairo -jpeg -r 150 "${filePath}" "${outputFilePrefix}"`;
    logger.info(`Executing PDF conversion command`);
    logger.debug({ command });
    try {
      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}:`);
      return { stdout, stderr };
    } catch (error) {
      const execError = error as Error & { stderr?: string };
      const errorMessage = `The pdftocairo command failed for file: ${filePath}.`;
      logger.error({ err: execError, stderr: execError.stderr }, errorMessage);
      throw new PdfConversionError(errorMessage, execError.stderr);
    }
  }

  /**
   * Scans the output directory for generated JPEG images and returns their paths.
   */
  private async _collectGeneratedImages(
    outputDir: string,
    outputFilePrefix: string,
    logger: Logger,
  ): Promise<string[]> {
    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.`,
    );

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

  /**
   * Converts a PDF file to a series of JPEG images using an external tool.
   */
  private async _convertPdfToImages(
    filePath: string,
    job: Job<FlyerJobData>,
    logger: Logger,
  ): Promise<string[]> {
    logger.info(`Starting PDF conversion for: ${filePath}`);

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

    const { stderr } = await this._executePdfConversion(filePath, outputFilePrefix, logger);

    const imagePaths = await this._collectGeneratedImages(outputDir, outputFilePrefix, logger);

    if (imagePaths.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 imagePaths;
  }

  /**
   * Processes a JPEG image to strip EXIF data by re-saving it.
   * This ensures user privacy and metadata consistency.
   * @returns The path to the newly created, processed JPEG file.
   */
  private async _stripExifDataFromJpeg(filePath: string, logger: Logger): Promise<string> {
    const outputDir = path.dirname(filePath);
    const originalFileName = path.parse(path.basename(filePath)).name;
    // Suffix to avoid overwriting, and keep extension.
    const newFileName = `${originalFileName}-processed.jpeg`;
    const outputPath = path.join(outputDir, newFileName);

    logger.info({ from: filePath, to: outputPath }, 'Processing JPEG to strip EXIF data.');

    try {
      // By default, sharp strips metadata when re-saving.
      // We also apply a reasonable quality setting for web optimization.
      await sharp(filePath).jpeg({ quality: 90 }).toFile(outputPath);
      return outputPath;
    } catch (error) {
      logger.error({ err: error, filePath }, 'Failed to process JPEG with sharp.');
      throw new ImageConversionError(`JPEG processing failed for ${path.basename(filePath)}.`);
    }
  }

  /**
   * Processes a PNG image to strip metadata by re-saving it.
   * @returns The path to the newly created, processed PNG file.
   */
  private async _stripMetadataFromPng(filePath: string, logger: Logger): Promise<string> {
    const outputDir = path.dirname(filePath);
    const originalFileName = path.parse(path.basename(filePath)).name;
    const newFileName = `${originalFileName}-processed.png`;
    const outputPath = path.join(outputDir, newFileName);

    logger.info({ from: filePath, to: outputPath }, 'Processing PNG to strip metadata.');

    try {
      // Re-saving with sharp strips metadata. We also apply a reasonable quality setting.
      await sharp(filePath).png({ quality: 90 }).toFile(outputPath);
      return outputPath;
    } catch (error) {
      logger.error({ err: error, filePath }, 'Failed to process PNG with sharp.');
      throw new ImageConversionError(`PNG processing failed for ${path.basename(filePath)}.`);
    }
  }

  /**
   * Converts an image file (e.g., GIF, TIFF) to a PNG format that the AI can process.
   */
  private async _convertImageToPng(filePath: string, logger: Logger): Promise<string> {
    const outputDir = path.dirname(filePath);
    const originalFileName = path.parse(path.basename(filePath)).name;
    const newFileName = `${originalFileName}-converted.png`;
    const outputPath = path.join(outputDir, newFileName);

    logger.info({ from: filePath, to: outputPath }, 'Converting unsupported image format to PNG.');

    try {
      await sharp(filePath).png().toFile(outputPath);
      return outputPath;
    } catch (error) {
      logger.error({ err: error, filePath }, 'Failed to convert image to PNG using sharp.');
      throw new ImageConversionError(`Image conversion to PNG failed for ${path.basename(filePath)}.`);
    }
  }

  /**
   * Handles PDF files by converting them to a series of JPEG images.
   */
  private async _handlePdfInput(
    filePath: string,
    job: Job<FlyerJobData>,
    logger: Logger,
  ): Promise<{ imagePaths: { path: string; mimetype: string }[]; createdImagePaths: string[] }> {
    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 };
  }

  /**
   * Handles image files that are directly supported by the AI.
   */
  private async _handleSupportedImageInput(
    filePath: string,
    fileExt: string,
    logger: Logger,
  ): Promise<{ imagePaths: { path: string; mimetype: string }[]; createdImagePaths: string[] }> {
    // For JPEGs, we will re-process them to strip EXIF data.
    if (fileExt === '.jpg' || fileExt === '.jpeg') {
      const processedPath = await this._stripExifDataFromJpeg(filePath, logger);
      return {
        imagePaths: [{ path: processedPath, mimetype: 'image/jpeg' }],
        // The original file will be cleaned up by the orchestrator, but we must also track this new file.
        createdImagePaths: [processedPath],
      };
    }

    // For PNGs, also re-process to strip metadata.
    if (fileExt === '.png') {
      const processedPath = await this._stripMetadataFromPng(filePath, logger);
      return {
        imagePaths: [{ path: processedPath, mimetype: 'image/png' }],
        createdImagePaths: [processedPath],
      };
    }

    // For other supported types like WEBP, etc., which are less likely to have problematic EXIF,
    // we can process them directly without modification for now.
    logger.info(`Processing as a single image file (non-JPEG/PNG): ${filePath}`);
    return { imagePaths: [{ path: filePath, mimetype: `image/${fileExt.slice(1)}` }], createdImagePaths: [] };
  }

  /**
   * Handles image files that need to be converted to PNG before AI processing.
   */
  private async _handleConvertibleImageInput(
    filePath: string,
    logger: Logger,
  ): Promise<{ imagePaths: { path: string; mimetype: string }[]; createdImagePaths: string[] }> {
    const createdPngPath = await this._convertImageToPng(filePath, logger);
    const imagePaths = [{ path: createdPngPath, mimetype: 'image/png' }];
    const createdImagePaths = [createdPngPath];
    return { imagePaths, createdImagePaths };
  }

  /**
   * Throws an error for unsupported file types.
   */
  private _handleUnsupportedInput(
    fileExt: string,
    originalFileName: string,
    logger: Logger,
  ): never {
    const errorMessage = `Unsupported file type: ${fileExt}. Supported types are PDF, JPG, PNG, WEBP, HEIC, HEIF, GIF, TIFF, SVG, BMP.`;
    logger.error({ originalFileName, fileExt }, errorMessage);
    throw new UnsupportedFileTypeError(errorMessage);
  }

  /**
   * Prepares the input images for the AI service. If the input is a PDF, it's converted to images.
   */
  public async prepareImageInputs(
    filePath: string,
    job: Job<FlyerJobData>,
    logger: Logger,
  ): Promise<{ imagePaths: { path: string; mimetype: string }[]; createdImagePaths: string[] }> {
    console.error(`[WORKER DEBUG] FlyerFileHandler: prepareImageInputs called for ${filePath}`);
    const fileExt = path.extname(filePath).toLowerCase();
    console.error(`[WORKER DEBUG] FlyerFileHandler: Detected extension: ${fileExt}`);

    if (fileExt === '.pdf') {
      return this._handlePdfInput(filePath, job, logger);
    }
    if (SUPPORTED_IMAGE_EXTENSIONS.includes(fileExt)) {
      return this._handleSupportedImageInput(filePath, fileExt, logger);
    }
    if (CONVERTIBLE_IMAGE_EXTENSIONS.includes(fileExt)) {
      return this._handleConvertibleImageInput(filePath, logger);
    }

    return this._handleUnsupportedInput(fileExt, job.data.originalFileName, logger);
  }

  /**
   * Optimizes images for web delivery (compression, resizing).
   * This is a distinct processing stage.
   */
  public async optimizeImages(
    imagePaths: { path: string; mimetype: string }[],
    logger: Logger,
  ): Promise<void> {
    logger.info(`Starting image optimization for ${imagePaths.length} images.`);

    for (const image of imagePaths) {
      const tempPath = `${image.path}.tmp`;
      try {
        // Optimize: Resize to max width 2000px (preserving aspect ratio) and compress
        await sharp(image.path)
          .resize({ width: 2000, withoutEnlargement: true })
          .jpeg({ quality: 80, mozjpeg: true }) // Use mozjpeg for better compression
          .toFile(tempPath);

        // Replace the original file with the optimized version
        await this.fs.rename(tempPath, image.path);
      } catch (error) {
        logger.error({ err: error, path: image.path }, 'Failed to optimize image.');
        throw new ImageConversionError(`Image optimization failed for ${path.basename(image.path)}.`);
      }
    }
    logger.info('Image optimization complete.');
  }
}