<?php

declare(strict_types=1);

namespace App\Services;

use App\Core\Logger;

/**
 * ImageProcessor — PHP GD-based Image Manipulation Engine (G005)
 *
 * Provides functions to:
 *   - Auto-orient images based on EXIF metadata (JPEG only).
 *   - Resize keeping aspect ratio (without upscaling).
 *   - Optional crop using coordinates (x, y, width, height).
 *   - Convert to WebP format with quality compression.
 *   - Strip metadata (EXIF/GPS/IPTC) implicitly via GD output.
 *   - Safely manage memory limits and destroy resources.
 *
 * @package Inveetaire
 * @ref     G005
 */
class ImageProcessor
{
    /**
     * WebP output quality (0 to 100).
     */
    private int $webpQuality;

    public function __construct()
    {
        $this->webpQuality = (int) config('storage.webp_quality', 80);
    }

    /**
     * Process an image uploaded to a specific slot and save it to the destination path as WebP.
     *
     * @param string $sourcePath      Path to the uploaded temporary file.
     * @param string $destinationPath Target absolute path to save the processed image.
     * @param array  $slotConfig      Slot configuration details containing limits and max_dimension.
     * @param array|null $cropParams  Optional crop parameters: ['x' => int, 'y' => int, 'width' => int, 'height' => int]
     * @return bool                   True on success.
     * @throws \RuntimeException      If image processing fails.
     */
    public function process(string $sourcePath, string $destinationPath, array $slotConfig, ?array $cropParams = null): bool
    {
        // 1. Memory Optimization: Temporarily raise memory limit for large images
        $this->optimizeMemoryLimit();

        // 2. Detect mime type to load image using appropriate GD function
        $mime = $this->detectMime($sourcePath);
        $image = $this->createImageResource($sourcePath, $mime);

        if (!$image) {
            Logger::error('ImageProcessor: Failed to load image resource', [
                'source' => $sourcePath,
                'mime' => $mime
            ]);
            throw new \RuntimeException('Failed to load image file. The file may be corrupt.');
        }

        try {
            // 3. Fix Orientation (JPEG only, uses EXIF)
            if ($mime === 'image/jpeg') {
                $image = $this->fixOrientation($image, $sourcePath);
            }

            // Ensure we work with truecolor and preserve alpha transparency channel
            if (function_exists('imagepalettetotruecolor')) {
                imagepalettetotruecolor($image);
            }
            imagealphablending($image, false);
            imagesavealpha($image, true);

            // 4. Optional Cropping (Server-side crop coordinates backup)
            if ($cropParams !== null && $this->validateCropParams($cropParams)) {
                $image = $this->crop($image, $cropParams);
            }

            // 5. Resize (Maintain aspect ratio, never upscale)
            if (isset($slotConfig['max_dimension'])) {
                $image = $this->resize($image, (int) $slotConfig['max_dimension']);
            }

            // 6. Convert & Compress to WebP (This implicitly strips metadata)
            $success = imagewebp($image, $destinationPath, $this->webpQuality);

            if (!$success) {
                Logger::error('ImageProcessor: imagewebp failed to write file', [
                    'destination' => $destinationPath
                ]);
                throw new \RuntimeException('Failed to write WebP image to disk.');
            }

            Logger::info('ImageProcessor: Image successfully processed to WebP', [
                'source' => $sourcePath,
                'destination' => $destinationPath,
                'slot_limit' => $slotConfig['max_dimension'] ?? 'none'
            ]);

            return true;

        } finally {
            // Always free image resource memory
            if (is_resource($image)) {
                imagedestroy($image);
            }
        }
    }

    /**
     * Fix orientation using EXIF header.
     * EXIF headers are only available in JPEG.
     */
    private function fixOrientation($image, string $filePath)
    {
        if (!function_exists('exif_read_data')) {
            return $image;
        }

        $exif = @exif_read_data($filePath);
        if (!$exif || !isset($exif['Orientation'])) {
            return $image;
        }

        $orientation = (int) $exif['Orientation'];
        $rotated = $image;

        switch ($orientation) {
            case 3: // 180 degrees
                $rotated = imagerotate($image, 180, 0);
                break;
            case 6: // 90 degrees clockwise (must rotate 270 degrees clockwise to fix)
                $rotated = imagerotate($image, 270, 0);
                break;
            case 8: // 90 degrees counter-clockwise (must rotate 90 degrees clockwise to fix)
                $rotated = imagerotate($image, 90, 0);
                break;
        }

        if ($rotated !== $image) {
            imagedestroy($image); // Free old unrotated image
        }

        return $rotated;
    }

    /**
     * Crop the image using coordinates.
     */
    private function crop($image, array $coords)
    {
        $cropped = imagecrop($image, [
            'x'      => (int) $coords['x'],
            'y'      => (int) $coords['y'],
            'width'  => (int) $coords['width'],
            'height' => (int) $coords['height']
        ]);

        if ($cropped) {
            imagedestroy($image); // Free original
            return $cropped;
        }

        return $image;
    }

    /**
     * Resize image ensuring aspect ratio is maintained. Never upscales.
     */
    private function resize($image, int $maxDimension)
    {
        $width  = imagesx($image);
        $height = imagesy($image);

        if ($width <= 0 || $height <= 0) {
            return $image;
        }

        $maxSide = max($width, $height);

        // Never upscale
        if ($maxSide <= $maxDimension) {
            return $image;
        }

        if ($width > $height) {
            $newWidth  = $maxDimension;
            $newHeight = (int) round(($height * $maxDimension) / $width);
        } else {
            $newHeight = $maxDimension;
            $newWidth  = (int) round(($width * $maxDimension) / $height);
        }

        $newImage = imagecreatetruecolor($newWidth, $newHeight);

        // Preserve transparency
        imagealphablending($newImage, false);
        imagesavealpha($newImage, true);

        // Perform resize
        imagecopyresampled($newImage, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
        imagedestroy($image); // Free original

        return $newImage;
    }

    /**
     * Create a GD image resource from path based on mime type.
     */
    private function createImageResource(string $path, string $mime)
    {
        switch ($mime) {
            case 'image/jpeg':
                return @imagecreatefromjpeg($path);
            case 'image/png':
                return @imagecreatefrompng($path);
            case 'image/webp':
                return @imagecreatefromwebp($path);
            default:
                return null;
        }
    }

    /**
     * Detect MIME type of a file.
     */
    private function detectMime(string $path): string
    {
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mime = (string) finfo_file($finfo, $path);
        finfo_close($finfo);
        return $mime;
    }

    /**
     * Ensure we don't run out of memory for large image loads.
     */
    private function optimizeMemoryLimit(): void
    {
        $currentLimit = ini_get('memory_limit');
        if ($currentLimit) {
            $limitInBytes = $this->convertToBytes($currentLimit);
            // If memory limit is less than 256MB, temporarily boost it
            if ($limitInBytes > 0 && $limitInBytes < 256 * 1024 * 1024) {
                @ini_set('memory_limit', '256M');
            }
        }
    }

    /**
     * Convert PHP ini shorthand notation to bytes.
     */
    private function convertToBytes(string $val): int
    {
        $val  = trim($val);
        $last = strtolower($val[strlen($val) - 1]);
        $num  = (int) $val;

        switch ($last) {
            case 'g':
                $num *= 1024;
            case 'm':
                $num *= 1024;
            case 'k':
                $num *= 1024;
        }

        return $num;
    }

    /**
     * Validate crop coordinates.
     */
    private function validateCropParams(array $coords): bool
    {
        return isset($coords['x'], $coords['y'], $coords['width'], $coords['height'])
            && (int) $coords['width'] > 0
            && (int) $coords['height'] > 0;
    }
}
