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',
]);

const MIN_WEBP_QUALITY = 50;
const WEBP_EFFORT = 4;

@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;
  private readonly skipImageProcessing: boolean;

  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);
    this.skipImageProcessing =
      this.configService.get<string>('SKIP_IMAGE_PROCESSING', 'false').toLowerCase() === 'true';
  }

  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;
    }

    if (this.skipImageProcessing) {
      this.logger.debug('Image processing skipped (SKIP_IMAGE_PROCESSING=true)');
      return null;
    }

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

      if (await this.shouldSkipAsWebp(buffer, mimetype, metadata)) {
        this.logger.debug(
          `Image processing skipped: webp within limits (${metadata.width}x${metadata.height}, ${buffer.length} bytes)`,
        );
        return null;
      }

      const targetDimensions = this.computeTargetDimensions(buffer.length, metadata);
      let quality = this.webpQuality;
      let output = await this.encodeWebp(buffer, animated, targetDimensions, quality);

      if (output.length > this.maxOutputBytes) {
        quality = this.estimateQuality(quality, output.length);
        output = await this.encodeWebp(buffer, animated, targetDimensions, quality);
      }

      if (output.length > this.maxOutputBytes && this.canShrinkDimensions(targetDimensions)) {
        const shrunk = this.shrinkDimensions(targetDimensions, output.length);
        quality = this.webpQuality;
        output = await this.encodeWebp(buffer, animated, shrunk, quality);
      }

      this.logger.log(
        `Image processed: ${metadata.width}x${metadata.height} -> ${targetDimensions.width}x${targetDimensions.height}, ` +
          `${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 async shouldSkipAsWebp(
    buffer: Buffer,
    mimetype: string,
    metadata: sharp.Metadata,
  ): Promise<boolean> {
    if (mimetype !== 'image/webp' || buffer.length > this.maxOutputBytes) {
      return false;
    }

    const { width, height } = metadata;
    if (!width || !height) {
      return false;
    }

    return width <= this.maxWidth && height <= this.maxHeight;
  }

  private computeTargetDimensions(
    bufferLength: number,
    metadata: sharp.Metadata,
  ): { width: number; height: number } {
    let width = metadata.width ?? 0;
    let height = metadata.height ?? 0;

    if (!width || !height) {
      return { width: 0, height: 0 };
    }

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

    if (bufferLength > this.maxOutputBytes) {
      const byteScale = Math.min(1, Math.sqrt((this.maxOutputBytes * 0.85) / bufferLength));
      if (byteScale < 1) {
        width = Math.max(1, Math.round(width * byteScale));
        height = Math.max(1, Math.round(height * byteScale));
      }
    }

    return { width, height };
  }

  private canShrinkDimensions(dimensions: { width: number; height: number }): boolean {
    const { width, height } = dimensions;
    return width > 0 && height > 0 && (width > 800 || height > 800);
  }

  private shrinkDimensions(
    dimensions: { width: number; height: number },
    outputBytes: number,
  ): { width: number; height: number } {
    const scale = Math.min(0.85, Math.sqrt((this.maxOutputBytes * 0.9) / outputBytes));
    return {
      width: Math.max(1, Math.round(dimensions.width * scale)),
      height: Math.max(1, Math.round(dimensions.height * scale)),
    };
  }

  private estimateQuality(currentQuality: number, outputBytes: number): number {
    const ratio = this.maxOutputBytes / outputBytes;
    return Math.max(MIN_WEBP_QUALITY, Math.min(currentQuality - 1, Math.floor(currentQuality * ratio * 0.95)));
  }

  private encodeWebp(
    buffer: Buffer,
    animated: boolean,
    dimensions: { width: number; height: number },
    quality: number,
  ): Promise<Buffer> {
    let pipeline = sharp(buffer, { animated }).rotate();

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

    return pipeline.webp({ quality, effort: WEBP_EFFORT }).toBuffer();
  }
}
