<?php
/**
 * Image Processing Service
 * Handles cropping and responsive image generation for block registry fields
 */
namespace Mtc\Modules\BlockRegistry\Services;

use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver;
use Mtc\Modules\BlockRegistry\Models\EntityBlock;
use Mtc\Modules\BlockRegistry\Models\BlockRegistryField;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;

class ImageProcessingService
{
    protected ImageManager $imageManager;
    protected string $cacheBasePath;
    protected int $defaultQuality = 85;

    public function __construct()
    {
        $this->imageManager = new ImageManager(new ImagickDriver());
        // Use base_path instead of public_path since document root is at project root
        $this->cacheBasePath = base_path('uploads/cache/blocks');

        // Ensure cache directory exists
        if (!File::exists($this->cacheBasePath)) {
            File::makeDirectory($this->cacheBasePath, 0755, true);
        }
    }

    /**
     * Process image with crop and generate srcset sizes
     *
     * @param EntityBlock $entityBlock The entity block being saved
     * @param BlockRegistryField $field The image field definition
     * @param array $imageData The submitted image data (url, alt, crop coordinates)
     * @return array Processed image data with srcset URLs
     */
    public function processImageField(
        EntityBlock $entityBlock,
        BlockRegistryField $field,
        array $imageData,
        ?int $rowIndex = null,
        ?string $parentFieldSlug = null
    ): array {
        // Validate image URL exists
        $originalUrl = $imageData['url'] ?? '';
        if (empty($originalUrl)) {
            return $imageData;
        }

        // Resolve to absolute path (with security validation)
        $originalPath = $this->resolveImagePath($originalUrl);
        if ($originalPath === null || !file_exists($originalPath)) {
            Log::warning("Image not found or invalid for processing: {$originalUrl}");
            return $imageData;
        }

        // Get field settings
        $settings = $this->parseFieldSettings($field);
        if (!$settings['crop_enabled']) {
            return $imageData;
        }

        // Extract crop coordinates
        $crop = $imageData['crop'] ?? [];
        $srcsetSizes = $settings['srcset_sizes'];
        $quality = $settings['quality'];

        // Create output directory for this entity block
        $outputDir = $this->cacheBasePath . '/' . $entityBlock->id;
        if (!File::exists($outputDir)) {
            File::makeDirectory($outputDir, 0755, true);
        }

        try {
            // Load original image
            $image = $this->imageManager->read($originalPath);

            // Store original dimensions
            $imageData['original_width'] = $image->width();
            $imageData['original_height'] = $image->height();

            // Apply crop if coordinates provided and valid
            $minSrcsetWidth = !empty($srcsetSizes) ? (int)min($srcsetSizes) : 0;
            $hasCropData = !empty($crop) && isset($crop['width']) && (int)$crop['width'] > 0;

            if ($this->isValidCrop($crop, $minSrcsetWidth)) {
                $image = $image->crop(
                    (int) $crop['width'],
                    (int) $crop['height'],
                    (int) $crop['x'],
                    (int) $crop['y']
                );
            } elseif ($hasCropData && $minSrcsetWidth > 0 && (int)$crop['width'] < $minSrcsetWidth) {
                // Crop was rejected due to minimum width — flag it for the controller
                $imageData['crop_rejected'] = true;
                $imageData['crop_rejected_reason'] = "Crop width ({$crop['width']}px) is below the minimum {$minSrcsetWidth}px required for responsive images. The original image was used instead.";
                Log::warning("Crop rejected for field: width {$crop['width']}px < minimum {$minSrcsetWidth}px", [
                    'field' => $field->name,
                    'entity_block_id' => $entityBlock->id,
                ]);
            }

            // Generate srcset sizes
            $processed = [];
            $filename = pathinfo($originalUrl, PATHINFO_FILENAME);
            $fieldSlug = $parentFieldSlug
                ? $this->slugify($parentFieldSlug) . '--' . $this->slugify($field->name)
                : $this->slugify($field->name);
            if ($rowIndex !== null) {
                $fieldSlug .= '-r' . $rowIndex;
            }

            // Generate cache-busting hash from crop coordinates and source URL
            $cropHash = substr(md5($originalUrl . json_encode($crop)), 0, 8);

            // Clean up old cached files for this field before creating new ones
            $this->cleanupFieldCache($outputDir, $fieldSlug);

            $croppedWidth = $image->width();

            // Filter srcset sizes to only those <= cropped width
            $validSizes = array_filter($srcsetSizes, function($size) use ($croppedWidth) {
                $size = (int)$size;
                return $size >= 50 && $size <= 5000 && $size <= $croppedWidth;
            });

            // If no valid sizes (crop is smaller than all srcset sizes), add the cropped width
            if (empty($validSizes) && $croppedWidth >= 50) {
                $validSizes = [$croppedWidth];
            }

            // Encode the cropped image once to re-read for each size
            // (clone is unsafe — Intervention Image v3 shares the underlying Imagick resource)
            $croppedBytes = (string) $image->toPng();

            // Detect transparency — use PNG output to preserve alpha, JPEG otherwise
            $hasTransparency = $this->imageHasTransparency($image);
            $outputExtension = $hasTransparency ? 'png' : 'jpg';

            foreach ($validSizes as $targetWidth) {
                $targetWidth = (int)$targetWidth;

                // Create a fresh image instance from the cropped bytes for each size
                $resized = $this->imageManager->read($croppedBytes);
                $resized = $resized->scaleDown(width: $targetWidth);

                // Generate unique filename with crop hash for cache busting
                $outputFilename = "{$fieldSlug}_{$targetWidth}w_{$cropHash}.{$outputExtension}";
                $outputPath = "{$outputDir}/{$outputFilename}";
                $outputUrl = "/uploads/cache/blocks/{$entityBlock->id}/{$outputFilename}";

                // Save — PNG for images with transparency, JPEG otherwise
                if ($hasTransparency) {
                    $resized->toPng()->save($outputPath);
                } else {
                    $resized->toJpeg($quality)->save($outputPath);
                }

                $processed[$targetWidth] = $outputUrl;
            }

            // Update image data with processed URLs
            $imageData['processed'] = $processed;

            // Build srcset string for convenience
            $srcsetParts = [];
            foreach ($processed as $width => $url) {
                $srcsetParts[] = "{$url} {$width}w";
            }
            $imageData['srcset'] = implode(', ', $srcsetParts);

        } catch (\Exception $e) {
            Log::error("Image processing failed: " . $e->getMessage(), [
                'field' => $field->name,
                'entity_block_id' => $entityBlock->id,
                'original_url' => $originalUrl,
            ]);
            // Return original data on error
        }

        return $imageData;
    }

    /**
     * Delete all processed images for an entity block
     *
     * @param int $entityBlockId The entity block ID
     */
    public function deleteProcessedImages(int $entityBlockId): void
    {
        $dir = $this->cacheBasePath . '/' . $entityBlockId;

        if (File::exists($dir)) {
            File::deleteDirectory($dir);
        }
    }

    /**
     * Clean up old cached files for a specific field
     * Removes files matching the field slug pattern before new ones are created
     *
     * @param string $outputDir The output directory
     * @param string $fieldSlug The field slug to match
     */
    protected function cleanupFieldCache(string $outputDir, string $fieldSlug): void
    {
        if (!File::exists($outputDir)) {
            return;
        }

        // Find and delete all files matching this field's pattern (both jpg and png)
        $oldFiles = array_merge(
            glob($outputDir . '/' . $fieldSlug . '_*.jpg') ?: [],
            glob($outputDir . '/' . $fieldSlug . '_*.png') ?: []
        );

        foreach ($oldFiles as $file) {
            File::delete($file);
        }
    }

    /**
     * Check if field has crop settings enabled
     *
     * @param BlockRegistryField $field
     * @return bool
     */
    public function fieldHasCropEnabled(BlockRegistryField $field): bool
    {
        $settings = $this->parseFieldSettings($field);
        return $settings['crop_enabled'];
    }

    /**
     * Get aspect ratio for field (returns width/height or null)
     *
     * @param BlockRegistryField $field
     * @return float|null
     */
    public function getAspectRatio(BlockRegistryField $field): ?float
    {
        $settings = $this->parseFieldSettings($field);
        $ratio = $settings['aspect_ratio'] ?? null;

        if (!$ratio || !str_contains($ratio, ':')) {
            return null;
        }

        [$w, $h] = explode(':', $ratio);
        $width = (float)$w;
        $height = (float)$h;

        // Prevent division by zero
        if ($height <= 0) {
            Log::warning("Invalid aspect ratio height in field settings: {$ratio}");
            return null;
        }

        return $width / $height;
    }

    /**
     * Parse field settings from JSON
     *
     * @param BlockRegistryField $field
     * @return array
     */
    protected function parseFieldSettings(BlockRegistryField $field): array
    {
        $settings = [];

        if (!empty($field->settings)) {
            $decoded = is_string($field->settings)
                ? json_decode($field->settings, true)
                : $field->settings;

            $settings = $decoded['image_settings'] ?? [];
        }

        // Return with defaults
        return [
            'crop_enabled' => $settings['crop_enabled'] ?? false,
            'aspect_ratio' => $settings['aspect_ratio'] ?? '',
            'srcset_sizes' => $settings['srcset_sizes'] ?? [400, 800, 1200, 1600],
            'quality' => (int)($settings['quality'] ?? $this->defaultQuality),
        ];
    }

    /**
     * Resolve image URL to absolute file path
     *
     * @param string $url
     * @return string|null Returns null if path is invalid or outside base directory
     */
    protected function resolveImagePath(string $url): ?string
    {
        // Handle absolute URLs
        if (str_starts_with($url, 'http://') || str_starts_with($url, 'https://')) {
            // Extract path from URL
            $parsed = parse_url($url);
            $url = $parsed['path'] ?? '';
        }

        // Remove leading slash and resolve from base path (document root is at project root)
        $relativePath = ltrim($url, '/');

        // Security: Prevent path traversal attacks
        // Reject paths with .. or that try to escape the base directory
        if (str_contains($relativePath, '..') || str_contains($relativePath, "\0")) {
            Log::warning("Potential path traversal attempt blocked: {$url}");
            return null;
        }

        $resolvedPath = base_path($relativePath);
        $realPath = realpath($resolvedPath);
        $basePath = realpath(base_path());

        // Ensure resolved path is within base directory
        if ($realPath === false || $basePath === false) {
            return null;
        }

        if (!str_starts_with($realPath, $basePath . DIRECTORY_SEPARATOR)) {
            Log::warning("Image path outside base directory blocked: {$url}");
            return null;
        }

        return $realPath;
    }

    /**
     * Check if crop coordinates are valid
     *
     * @param array $crop
     * @return bool
     */
    protected function isValidCrop(array $crop, int $minWidth = 0): bool
    {
        if (empty($crop)) {
            return false;
        }

        $required = ['x', 'y', 'width', 'height'];
        foreach ($required as $key) {
            if (!isset($crop[$key])) {
                return false;
            }
        }

        $x = (int)$crop['x'];
        $y = (int)$crop['y'];
        $width = (int)$crop['width'];
        $height = (int)$crop['height'];

        // Coordinates must not be negative
        if ($x < 0 || $y < 0) {
            return false;
        }

        // Must have positive dimensions
        if ($width <= 0 || $height <= 0) {
            return false;
        }

        // Crop must meet minimum width for srcset generation
        if ($minWidth > 0 && $width < $minWidth) {
            return false;
        }

        return true;
    }

    /**
     * Check if an Intervention Image has any transparent pixels
     *
     * @param \Intervention\Image\Image $image
     * @return bool
     */
    protected function imageHasTransparency(\Intervention\Image\Image $image): bool
    {
        $core = $image->core()->native();

        if ($core instanceof \Imagick) {
            // Quick check: if image has no alpha channel at all, skip further inspection
            if (!$core->getImageAlphaChannel()) {
                return false;
            }

            // Exact, single-pass check: inspect the alpha channel's value range.
            // If the minimum alpha is below the maximum (fully opaque) value, at
            // least one pixel is transparent. This is both accurate and cheap —
            // unlike pixel sampling, it can never miss sparse transparent regions.
            $alphaRange = $core->getImageChannelRange(\Imagick::CHANNEL_ALPHA);
            $quantumRange = $core->getQuantumRange();

            return $alphaRange['minima'] < $quantumRange['quantumRangeLong'];
        }

        return false;
    }

    /**
     * Create URL-safe slug from string
     *
     * @param string $text
     * @return string
     */
    protected function slugify(string $text): string
    {
        // Convert to lowercase
        $text = strtolower($text);
        // Replace spaces and underscores with hyphens
        $text = preg_replace('/[\s_]+/', '-', $text);
        // Remove non-alphanumeric characters except hyphens
        $text = preg_replace('/[^a-z0-9\-]/', '', $text);
        // Remove multiple consecutive hyphens
        $text = preg_replace('/-+/', '-', $text);
        // Trim hyphens from ends
        return trim($text, '-');
    }
}
