<?php
namespace EasyChat\Library;








class ImageVariantService {

    const CANONICAL_MAX_WIDTH = 1920;
    const CANONICAL_MAX_HEIGHT = 1920;
    const PREVIEW_MAX_WIDTH = 1280;
    const PREVIEW_MAX_HEIGHT = 1280;
    const THUMB_MAX_WIDTH = 360;
    const THUMB_MAX_HEIGHT = 260;
    const AI_MAX_WIDTH = 1024;
    const AI_MAX_HEIGHT = 1024;
    const AI_MAX_BYTES = 716800;
    const AI_MAX_SOURCE_PIXELS = 25000000;

    const WEBP_QUALITY = 82;
    const JPEG_QUALITY = 84;
    const THUMB_WEBP_QUALITY = 76;
    const THUMB_JPEG_QUALITY = 78;

    public static function supportsWebp(): bool {
        return function_exists('imagewebp');
    }

    public static function preferredFormat(): string {
        return self::supportsWebp() ? 'webp' : 'jpg';
    }

    public static function mimeForFormat(string $format): string {
        return strtolower($format) === 'webp' ? 'image/webp' : 'image/jpeg';
    }

    public function isCanonicalFilename(string $filename): bool {
        return preg_match('/__ec_canonical\.(webp|jpg)$/i', basename($filename)) === 1;
    }

    public function processIncomingImage(int $chat_id, string $source_path, string $source_filename, bool $delete_source = true): array {
        $source_filename = basename($source_filename);
        if ($source_filename === '' || !is_file($source_path)) {
            return ['success' => false, 'error' => 'source_missing'];
        }

        $info = $this->imageInfo($source_path);
        if (!$info) {
            return $this->fallbackResult($source_path, $source_filename, 'decode_failed');
        }

        $source_mime = (string)$info['mime'];
        $source_size = (int)filesize($source_path);

        if ($source_mime === 'image/gif' && $this->isAnimatedGif($source_path)) {
            $variants = $this->ensureVariantsForFile($chat_id, $source_filename);
            return $this->mergeResult($source_path, $source_filename, $source_mime, $source_size, $info, $variants, [
                'canonical_mode' => 'animated_gif_original',
            ]);
        }

        if (!$this->canDecode($source_mime)) {
            return $this->fallbackResult($source_path, $source_filename, 'decoder_missing', $info);
        }

        $format = self::preferredFormat();
        $canonical_filename = $this->variantFilename($source_filename, 'canonical', $format);
        $canonical_path = dirname($source_path) . DS . $canonical_filename;
        $quality = $format === 'webp' ? self::WEBP_QUALITY : self::JPEG_QUALITY;

        $canonical = $this->writeResized($source_path, $canonical_path, self::CANONICAL_MAX_WIDTH, self::CANONICAL_MAX_HEIGHT, $format, $quality);
        if (!$canonical['success']) {
            return $this->fallbackResult($source_path, $source_filename, $canonical['error'] ?? 'canonical_failed', $info);
        }

        if ($delete_source && $canonical_path !== $source_path && is_file($canonical_path)) {
            SafeRuntime::unlink($source_path);
        }

        $variants = $this->ensureVariantsForFile($chat_id, $canonical_filename);
        $canonical_size = (int)filesize($canonical_path);

        return [
            'success' => true,
            'filename' => $canonical_filename,
            'path' => $canonical_path,
            'mime' => self::mimeForFormat($format),
            'extension' => $format,
            'size' => $canonical_size,
            'variants' => array_merge($variants, [
                'canonical' => [
                    'file' => $canonical_filename,
                    'width' => (int)$canonical['width'],
                    'height' => (int)$canonical['height'],
                    'mime' => self::mimeForFormat($format),
                    'size' => $canonical_size,
                ],
                'source' => [
                    'mime' => $source_mime,
                    'size' => $source_size,
                    'width' => (int)$info[0],
                    'height' => (int)$info[1],
                ],
                'canonical_mode' => $format,
            ]),
        ];
    }

    public function canonicalizeExistingImage(int $chat_id, string $source_filename): array {
        $source_filename = basename(trim($source_filename));
        $source_path = $this->chatUploadDir($chat_id) . DS . $source_filename;
        if ($source_filename === '' || !is_file($source_path)) {
            return ['success' => false, 'error' => 'source_missing'];
        }

        $info = $this->imageInfo($source_path);
        if (!$info) {
            return $this->fallbackResult($source_path, $source_filename, 'decode_failed');
        }

        $source_mime = (string)$info['mime'];
        $source_size = (int)filesize($source_path);

         
         
        if ($this->isCanonicalFilename($source_filename) || ($source_mime === 'image/gif' && $this->isAnimatedGif($source_path))) {
            $variants = $this->ensureVariantsForFile($chat_id, $source_filename);
            return $this->mergeResult($source_path, $source_filename, $source_mime, $source_size, $info, $variants, [
                'canonical_mode' => $this->isCanonicalFilename($source_filename) ? 'existing_canonical' : 'animated_gif_original',
            ]);
        }

        return $this->processIncomingImage($chat_id, $source_path, $source_filename, false);
    }

    public function ensureVariantsForFile(int $chat_id, string $filename): array {
        $filename = basename(trim($filename));
        $source_path = $this->chatUploadDir($chat_id) . DS . $filename;
        if ($filename === '' || !is_file($source_path)) {
            return ['status' => 'missing'];
        }

        $info = $this->imageInfo($source_path);
        if (!$info) {
            return ['status' => 'unsupported', 'error' => 'decode_failed'];
        }

        $format = self::preferredFormat();
        $source_mime = (string)$info['mime'];
        if (!$this->canDecode($source_mime)) {
            return [
                'status' => 'unsupported',
                'error' => 'decoder_missing',
                'source' => [
                    'mime' => $source_mime,
                    'size' => (int)filesize($source_path),
                    'width' => (int)$info[0],
                    'height' => (int)$info[1],
                ],
            ];
        }

        $thumb_filename = $this->variantFilename($filename, 'thumb', $format);
        $preview_filename = $this->variantFilename($filename, 'preview', $format);
        $dir = dirname($source_path);

        $thumb_path = $dir . DS . $thumb_filename;
        $preview_path = $dir . DS . $preview_filename;

        $thumb = is_file($thumb_path)
            ? $this->existingVariantInfo($thumb_path)
            : $this->writeResized($source_path, $thumb_path, self::THUMB_MAX_WIDTH, self::THUMB_MAX_HEIGHT, $format, $format === 'webp' ? self::THUMB_WEBP_QUALITY : self::THUMB_JPEG_QUALITY);
        $preview = is_file($preview_path)
            ? $this->existingVariantInfo($preview_path)
            : $this->writeResized($source_path, $preview_path, self::PREVIEW_MAX_WIDTH, self::PREVIEW_MAX_HEIGHT, $format, $format === 'webp' ? self::WEBP_QUALITY : self::JPEG_QUALITY);

        $status = (!empty($thumb['success']) && !empty($preview['success'])) ? 'ready' : 'partial';

        return [
            'status' => $status,
            'format' => $format,
            'source' => [
                'mime' => $source_mime,
                'size' => (int)filesize($source_path),
                'width' => (int)$info[0],
                'height' => (int)$info[1],
            ],
            'thumb' => !empty($thumb['success']) ? [
                'file' => $thumb_filename,
                'width' => (int)$thumb['width'],
                'height' => (int)$thumb['height'],
                'mime' => self::mimeForFormat($format),
                'size' => (int)filesize($thumb_path),
            ] : null,
            'preview' => !empty($preview['success']) ? [
                'file' => $preview_filename,
                'width' => (int)$preview['width'],
                'height' => (int)$preview['height'],
                'mime' => self::mimeForFormat($format),
                'size' => (int)filesize($preview_path),
            ] : null,
        ];
    }

    public function variantFilename(string $filename, string $variant, string $format): string {
        $filename = basename($filename);
        $base = pathinfo($filename, PATHINFO_FILENAME);
        $base = preg_replace('/[^a-zA-Z0-9_\-.]/', '_', $base);
        $variant = preg_replace('/[^a-zA-Z0-9_\-]/', '', $variant);
        $format = strtolower($format) === 'webp' ? 'webp' : 'jpg';
        return $base . '__ec_' . $variant . '.' . $format;
    }

    public function createJpegFallback(string $source_path, string $source_filename): array {
        $source_filename = basename($source_filename);
        if ($source_filename === '' || !is_file($source_path)) {
            return ['success' => false, 'error' => 'source_missing'];
        }

        $info = $this->imageInfo($source_path);
        if (!$info || !$this->canDecode((string)$info['mime'])) {
            return ['success' => false, 'error' => 'decoder_missing'];
        }
        $source_pixels = (int)$info[0] * (int)$info[1];
        if ((int)$info[0] <= 0 || (int)$info[1] <= 0 || $source_pixels > self::AI_MAX_SOURCE_PIXELS) {
             
             
            return ['success' => false, 'error' => 'source_dimensions_too_large'];
        }

        $base = pathinfo($source_filename, PATHINFO_FILENAME);
        $base = preg_replace('/[^a-zA-Z0-9_\-.]/', '_', $base);
        $filename = $base . '__ec_provider_jpeg_' . time() . '_' . bin2hex(random_bytes(4)) . '.jpg';
        $path = dirname($source_path) . DS . $filename;
        $result = $this->writeResized($source_path, $path, self::CANONICAL_MAX_WIDTH, self::CANONICAL_MAX_HEIGHT, 'jpg', self::JPEG_QUALITY);

        if (empty($result['success'])) {
            return $result;
        }

        return [
            'success' => true,
            'filename' => $filename,
            'path' => $path,
            'mime' => 'image/jpeg',
            'size' => (int)filesize($path),
            'temporary' => true,
        ];
    }

    






    public function ensureAiVariantForFile(int $chat_id, string $filename): array {
        $filename = basename(trim($filename));
        $chat_dir = $this->chatUploadDir($chat_id);
        $source_path = $chat_dir . DS . $filename;
        if ($chat_id <= 0 || $filename === '' || !is_file($source_path) || is_link($source_path)) {
            return ['success' => false, 'error' => 'source_missing'];
        }
        if (!$this->pathBelongsToDirectory($source_path, $chat_dir)) {
            return ['success' => false, 'error' => 'unsafe_source_path'];
        }

        $info = $this->imageInfo($source_path);
        if (!$info || !$this->canDecode((string)$info['mime'])) {
            return ['success' => false, 'error' => 'decoder_missing'];
        }
        $source_pixels = (int)$info[0] * (int)$info[1];
        if ((int)$info[0] <= 0 || (int)$info[1] <= 0 || $source_pixels > self::AI_MAX_SOURCE_PIXELS) {
             
             
             
            return ['success' => false, 'error' => 'source_dimensions_too_large'];
        }

        $ai_filename = $this->variantFilename($filename, 'ai', 'jpg');
        $ai_path = $chat_dir . DS . $ai_filename;
        $lock_path = $ai_path . '.lock';
        $lock = SafeRuntime::call(function () use ($lock_path) {
            return fopen($lock_path, 'c');
        }, false);
        if (!$lock || !flock($lock, LOCK_EX)) {
            if (is_resource($lock)) {
                fclose($lock);
            }
            return ['success' => false, 'error' => 'variant_lock_failed'];
        }

        try {
            $existing = $this->imageInfo($ai_path);
            if (
                $existing
                && (string)$existing['mime'] === 'image/jpeg'
                && (int)$existing[0] <= self::AI_MAX_WIDTH
                && (int)$existing[1] <= self::AI_MAX_HEIGHT
                && (int)filesize($ai_path) <= self::AI_MAX_BYTES
                && (int)filemtime($ai_path) >= (int)filemtime($source_path)
            ) {
                return $this->aiVariantResult($ai_path, $ai_filename, $existing);
            }

            $attempts = [
                [self::AI_MAX_WIDTH, self::AI_MAX_HEIGHT, 82],
                [self::AI_MAX_WIDTH, self::AI_MAX_HEIGHT, 70],
                [768, 768, 68],
                [640, 640, 64],
                [512, 512, 60],
            ];
            foreach ($attempts as $attempt) {
                $result = $this->writeResized(
                    $source_path,
                    $ai_path,
                    (int)$attempt[0],
                    (int)$attempt[1],
                    'jpg',
                    (int)$attempt[2]
                );
                if (
                    !empty($result['success'])
                    && is_file($ai_path)
                    && (int)filesize($ai_path) <= self::AI_MAX_BYTES
                ) {
                    $written_info = $this->imageInfo($ai_path);
                    if ($written_info && (string)$written_info['mime'] === 'image/jpeg') {
                        return $this->aiVariantResult($ai_path, $ai_filename, $written_info);
                    }
                }
            }
        } finally {
            flock($lock, LOCK_UN);
            fclose($lock);
             
             
             
        }

        SafeRuntime::unlink($ai_path);
        return ['success' => false, 'error' => 'ai_variant_too_large'];
    }

    private function aiVariantResult(string $path, string $filename, array $info): array {
        $hash = hash_file('sha256', $path);
        if (!is_string($hash) || $hash === '') {
            return ['success' => false, 'error' => 'variant_hash_failed'];
        }
        return [
            'success' => true,
            'filename' => $filename,
            'path' => $path,
            'mime' => 'image/jpeg',
            'size' => (int)filesize($path),
            'width' => (int)$info[0],
            'height' => (int)$info[1],
            'fingerprint' => $hash,
        ];
    }

    private function pathBelongsToDirectory(string $path, string $directory): bool {
        $real_path = realpath($path);
        $real_directory = realpath($directory);
        if ($real_path === false || $real_directory === false) {
            return false;
        }
        $prefix = rtrim($real_directory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
        return strpos($real_path, $prefix) === 0;
    }

    private function writeResized(string $source, string $destination, int $max_width, int $max_height, string $format, int $quality): array {
        $info = $this->imageInfo($source);
        if (!$info) {
            return ['success' => false, 'error' => 'decode_failed'];
        }

        $image = $this->createImage($source, (string)$info['mime']);
        if (!$image) {
            return ['success' => false, 'error' => 'decoder_missing'];
        }

        $orig_width = imagesx($image);
        $orig_height = imagesy($image);
        if ($orig_width <= 0 || $orig_height <= 0) {
            $this->destroyImage($image);
            return ['success' => false, 'error' => 'invalid_dimensions'];
        }

        $ratio = min($max_width / $orig_width, $max_height / $orig_height, 1);
        $width = max(1, (int)round($orig_width * $ratio));
        $height = max(1, (int)round($orig_height * $ratio));

        $new_image = imagecreatetruecolor($width, $height);
        if ($format === 'webp') {
            imagealphablending($new_image, false);
            imagesavealpha($new_image, true);
            $transparent = imagecolorallocatealpha($new_image, 255, 255, 255, 127);
            imagefilledrectangle($new_image, 0, 0, $width, $height, $transparent);
        } else {
            $white = imagecolorallocate($new_image, 255, 255, 255);
            imagefilledrectangle($new_image, 0, 0, $width, $height, $white);
        }

        imagecopyresampled($new_image, $image, 0, 0, 0, 0, $width, $height, $orig_width, $orig_height);

        $tmp = $destination . '.tmp';
        $ok = false;
        if ($format === 'webp') {
            $ok = function_exists('imagewebp') && imagewebp($new_image, $tmp, $quality);
        } else {
            $ok = imagejpeg($new_image, $tmp, $quality);
        }

        $this->destroyImage($image);
        $this->destroyImage($new_image);

        if (!$ok || !is_file($tmp)) {
            SafeRuntime::unlink($tmp);
            return ['success' => false, 'error' => 'encode_failed'];
        }

        if (!SafeRuntime::rename($tmp, $destination)) {
            SafeRuntime::unlink($tmp);
            return ['success' => false, 'error' => 'save_failed'];
        }

        return [
            'success' => true,
            'width' => $width,
            'height' => $height,
        ];
    }

    private function existingVariantInfo(string $path): array {
        $info = $this->imageInfo($path);
        if (!$info) {
            return ['success' => false, 'error' => 'decode_failed'];
        }
        return [
            'success' => true,
            'width' => (int)$info[0],
            'height' => (int)$info[1],
        ];
    }

    private function createImage(string $path, string $mime) {
        switch ($mime) {
            case 'image/jpeg':
                $image = function_exists('imagecreatefromjpeg') ? SafeRuntime::call(function () use ($path) { return imagecreatefromjpeg($path); }, false) : false;
                if ($image && function_exists('exif_read_data')) {
                    $exif = SafeRuntime::call(function () use ($path) { return exif_read_data($path); }, false);
                    if (is_array($exif) && isset($exif['Orientation'])) {
                        $orientation = (int)$exif['Orientation'];
                        if ($orientation === 6 || $orientation === 5) {
                            $rotated = function_exists('imagerotate') ? SafeRuntime::call(function () use ($image) { return imagerotate($image, 270, 0); }, false) : false;
                            if ($rotated) {
                                $this->destroyImage($image);
                                $image = $rotated;
                            }
                        } elseif ($orientation === 3 || $orientation === 4) {
                            $rotated = function_exists('imagerotate') ? SafeRuntime::call(function () use ($image) { return imagerotate($image, 180, 0); }, false) : false;
                            if ($rotated) {
                                $this->destroyImage($image);
                                $image = $rotated;
                            }
                        } elseif ($orientation === 8 || $orientation === 7) {
                            $rotated = function_exists('imagerotate') ? SafeRuntime::call(function () use ($image) { return imagerotate($image, 90, 0); }, false) : false;
                            if ($rotated) {
                                $this->destroyImage($image);
                                $image = $rotated;
                            }
                        }
                    }
                }
                return $image;
            case 'image/png':
                return function_exists('imagecreatefrompng') ? SafeRuntime::call(function () use ($path) { return imagecreatefrompng($path); }, false) : false;
            case 'image/gif':
                return function_exists('imagecreatefromgif') ? SafeRuntime::call(function () use ($path) { return imagecreatefromgif($path); }, false) : false;
            case 'image/bmp':
            case 'image/x-ms-bmp':
                return function_exists('imagecreatefrombmp') ? SafeRuntime::call(function () use ($path) { return imagecreatefrombmp($path); }, false) : false;
            case 'image/webp':
                return function_exists('imagecreatefromwebp') ? SafeRuntime::call(function () use ($path) { return imagecreatefromwebp($path); }, false) : false;
            default:
                return false;
        }
    }

    private function destroyImage($image): void {
         
        if (function_exists('imagedestroy') && defined('PHP_VERSION_ID') && PHP_VERSION_ID < 80500) {
            SafeRuntime::call(function () use ($image) {
                imagedestroy($image);
                return true;
            }, false);
        }
    }

    private function canDecode(string $mime): bool {
        switch ($mime) {
            case 'image/jpeg':
                return function_exists('imagecreatefromjpeg');
            case 'image/png':
                return function_exists('imagecreatefrompng');
            case 'image/gif':
                return function_exists('imagecreatefromgif');
            case 'image/bmp':
            case 'image/x-ms-bmp':
                return function_exists('imagecreatefrombmp');
            case 'image/webp':
                return function_exists('imagecreatefromwebp');
            default:
                return false;
        }
    }

    private function imageInfo(string $path) {
        return SafeRuntime::imageInfo($path);
    }

    private function isAnimatedGif(string $path): bool {
        $content = SafeRuntime::readFile($path);
        if (!is_string($content) || $content === '') {
            return false;
        }
        return preg_match_all('#\x00\x21\xF9\x04#s', $content) > 1;
    }

    private function fallbackResult(string $path, string $filename, string $error, $info = null): array {
        $mime = AttachmentPolicy::detectMime($path, pathinfo($filename, PATHINFO_EXTENSION));
        $size = is_file($path) ? (int)filesize($path) : 0;
        return [
            'success' => true,
            'filename' => $filename,
            'path' => $path,
            'mime' => $mime,
            'extension' => strtolower(pathinfo($filename, PATHINFO_EXTENSION)),
            'size' => $size,
            'variants' => [
                'status' => 'unsupported',
                'error' => $error,
                'canonical' => [
                    'file' => $filename,
                    'width' => is_array($info) ? (int)$info[0] : 0,
                    'height' => is_array($info) ? (int)$info[1] : 0,
                    'mime' => $mime,
                    'size' => $size,
                ],
            ],
        ];
    }

    private function mergeResult(string $path, string $filename, string $mime, int $size, array $info, array $variants, array $extra): array {
        return [
            'success' => true,
            'filename' => $filename,
            'path' => $path,
            'mime' => $mime,
            'extension' => strtolower(pathinfo($filename, PATHINFO_EXTENSION)),
            'size' => $size,
            'variants' => array_merge($variants, $extra, [
                'canonical' => [
                    'file' => $filename,
                    'width' => (int)$info[0],
                    'height' => (int)$info[1],
                    'mime' => $mime,
                    'size' => $size,
                ],
            ]),
        ];
    }

    private function chatUploadDir(int $chat_id): string {
        return rtrim(DIR_IMAGE, DS) . DS . 'chat_id_' . (int)$chat_id;
    }
}
