import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import sharp from 'sharp';

export interface ProcessedImage {
  buffer: Buffer;
  contentType: 'image/webp';
  extension: 'webp';
}

const RASTER_IMAGE_MIME_TYPES = new Set([
  'image/jpeg',
  'image/png',
  'image/gif',
  'image/webp',
  'image/tiff',
  'image/bmp',
]);

@Injectable()
export class ImageProcessingService {
  private readonly logger = new Logger(ImageProcessingService.name);
  private readonly maxWidth: number;
  private readonly maxHeight: number;
  private readonly maxOutputBytes: number;
  private readonly webpQuality: number;

  constructor(private readonly configService: ConfigService) {
    this.maxWidth = this.configService.get<number>('IMAGE_MAX_WIDTH', 1920);
    this.maxHeight = this.configService.get<number>('IMAGE_MAX_HEIGHT', 1920);
    this.maxOutputBytes = this.configService.get<number>('IMAGE_MAX_SIZE', 2 * 1024 * 1024);
    this.webpQuality = this.configService.get<number>('IMAGE_WEBP_QUALITY', 80);
  }

  shouldProcess(mimetype: string): boolean {
    return RASTER_IMAGE_MIME_TYPES.has(mimetype);
  }

  async processImage(buffer: Buffer, mimetype: string): Promise<ProcessedImage | null> {
    if (!this.shouldProcess(mimetype)) {
      return null;
    }

    try {
      const source = sharp(buffer, { animated: mimetype === 'image/gif' }).rotate();
      const metadata = await source.metadata();

      let targetWidth = metadata.width;
      let targetHeight = metadata.height;

      if (targetWidth && targetHeight && (targetWidth > this.maxWidth || targetHeight > this.maxHeight)) {
        const scale = Math.min(this.maxWidth / targetWidth, this.maxHeight / targetHeight);
        targetWidth = Math.round(targetWidth * scale);
        targetHeight = Math.round(targetHeight * scale);
      }

      let quality = this.webpQuality;
      let output = await this.renderWebp(source, targetWidth, targetHeight, quality);

      while (output.length > this.maxOutputBytes) {
        if (quality > 50) {
          quality -= 10;
          output = await this.renderWebp(source, targetWidth, targetHeight, quality);
          continue;
        }

        if (!targetWidth || !targetHeight || (targetWidth <= 800 && targetHeight <= 800)) {
          break;
        }

        targetWidth = Math.round(targetWidth * 0.85);
        targetHeight = Math.round(targetHeight * 0.85);
        quality = this.webpQuality;
        output = await this.renderWebp(source, targetWidth, targetHeight, quality);
      }

      this.logger.log(
        `Image processed: ${metadata.width}x${metadata.height} -> ${targetWidth}x${targetHeight}, ` +
          `${buffer.length} -> ${output.length} bytes (webp q${quality})`,
      );

      return {
        buffer: output,
        contentType: 'image/webp',
        extension: 'webp',
      };
    } catch (error: unknown) {
      this.logger.error(`Failed to process image: ${(error as Error).message}`, (error as Error).stack);
      throw error;
    }
  }

  private renderWebp(
    source: sharp.Sharp,
    width: number | undefined,
    height: number | undefined,
    quality: number,
  ): Promise<Buffer> {
    let pipeline = source.clone().rotate();

    if (width && height) {
      pipeline = pipeline.resize(width, height, { fit: 'inside', withoutEnlargement: true });
    }

    return pipeline.webp({ quality }).toBuffer();
  }
}
