<?php

/**
 * JCOGS Image Pro - Output Pipeline Stage
 * =======================================
 * Native EE7 implementation pipeline architecture
 * 
 * @category   ExpressionEngine Add-on
 * @package    JCOGS Image Pro
 * @author     JCOGS Design <contact@jcogs.net>
 * @copyright  Copyright (c) 2021 - 2025 JCOGS Design
 * @license    https://jcogs.net/add-ons/license/jcogs_img_pro
 * @version    2.0.6
 * @link       https://JCOGS.net/
 * @since      2.0.0
 */

namespace JCOGSDesign\JCOGSImagePro\Service\Pipeline;

use JCOGSDesign\JCOGSImagePro\Service\Utilities;
use Imagine\Gd\Imagine;
use JCOGSDesign\JCOGSImagePro\Service\Pipeline\ResponsiveImageService;
use JCOGSDesign\JCOGSImagePro\Service\Pipeline\OutputGenerationService;

/**
 * Output Pipeline Stage
 * 
 * Final stage of the processing pipeline. Generates the final HTML output
 * for the template, including IMG tags, variables, and metadata.
 * 
 * Responsibilities:
 * - Generate HTML IMG tags with lazy loading support
 * - Create responsive image attributes (srcset/sizes)
 * - Create template variables for pair tags
 * - Format output based on tag type
 * - Handle custom attributes and performance optimisations
 * - Generate final output string
 */
class OutputStage extends AbstractStage 
{
    /**
     * @var ResponsiveImageService
     */
    private ResponsiveImageService $responsive_service;
    
    /**
     * @var OutputGenerationService
     */
    private OutputGenerationService $output_generation_service;

    private ?Context $context = null;

    /**
     * @var mixed Image utilities service
     */
    private $image_utilities;
    
    /**
     * Constructor
     * 
     * Common services are now automatically available via parent AbstractStage.
     * Only initialize stage-specific services here.
     */
    public function __construct() 
    {
        parent::__construct('output');
        
        // Initialize stage-specific services that are not in common service cache
        $this->responsive_service = ee('jcogs_img_pro:ResponsiveImageService');
        $this->output_generation_service = ee('jcogs_img_pro:OutputGenerationService');
        $this->image_utilities = ee('jcogs_img_pro:ImageUtilities');
        
        // Common services like utilities, filesystem_service, etc. are available via parent
        // Note: lazy_loading_service accessed via helper method to prevent circular dependency
    }
    
    /**
     * Process output generation stage
     * 
     * @param Context $context Processing context
     * @throws \Exception If output generation fails
     */
    protected function process(Context $context): void 
    {
        // Cache context for this stage
        $this->context = $context;
        
        try {
            $output_start_time = microtime(true);
            
            $this->utilities_service->debug_message('Starting output stage', null, false, 'detailed');

            // 1. Check if output was already generated (e.g., SVG passthrough)
            if (!empty($context->get_output())) {
                $this->utilities_service->debug_message('Output already generated', null, false, 'detailed');
                return;
            }
            
            // 2. Ensure template variables are complete with valid dimensions
            // Skip dimension validation for cached images that already have valid dimensions
            $width = $context->get_metadata_value('width', 0);
            $height = $context->get_metadata_value('height', 0);
            $is_cached = $context->get_flag('using_cache_copy', false);
            
            // Only validate/recover dimensions if: NOT cached OR dimensions are missing/zero
            if (!$is_cached || $width === 0 || $height === 0) {
                $this->_ensure_template_variables_complete();
            }
        
        // 3. Determine output type based on context
        $output_type = $this->_determine_output_type();
        $this->utilities_service->debug_message('Output type determined: ' . $output_type, null, false, 'detailed');
        
        // 3. Generate appropriate output
        switch ($output_type) {
            case 'act_image_serve':
                $this->_serve_act_image();
                return; // Exit immediately after serving image data
                
            case 'img_tag':
                $output = $this->_generate_img_tag();
                break;
                
            case 'pair_variables':
                $output = $this->_generate_pair_variables();
                break;
                
            case 'palette_variables':
                $output = $this->_generate_palette_variables();
                break;
                
            case 'url_only':
                $output = $this->_generate_url_only();
                break;
                
            case 'no_output':
                // create_tag='n' - return empty output (Legacy line 963: "do nothing")
                $output = '';
                break;
                
            default:
                $output = $this->_generate_img_tag();
        }
        
        // 4. Set final output
        $context->set_output($output);
        
        $this->utilities_service->debug_message('Output stage completed', null, false, 'detailed');

        // Post-processing timing removed (always ~0.000s, not informative)

        $this->utilities_service->debug_message('Generating tag outputs', null, false, 'detailed');
        
        $generation_start_time = microtime(true);
        // Add minimal time for generation (legacy shows this as ~0.0000 seconds)
        $generation_time = microtime(true) - $generation_start_time;
        $this->utilities_service->debug_message('Generation completed for output in ' . number_format($generation_time, 3) . ' seconds', null, false, 'detailed');
        } finally {
            // Clean up context reference
            $this->context = null;
        }
    }
    
    /**
     * Apply image path prefix to URL
     * 
     * Adds the image_path_prefix parameter to the URL construction,
     * mirroring legacy path handling behavior from the original addon.
     * 
     * Priority order:
     * 1. Connection config path_prefix (from named connection CDN settings)
     * 2. image_path_prefix parameter (from tag parameter)
     * 
     * @param string $base_url Original image URL
     * @return string URL with prefix applied
     */
    private function _apply_image_path_prefix(string $base_url): string
    {
        $path_prefix = '';
        
        // First priority: Check connection configuration for path_prefix
        $connection_name = $this->context->get_metadata_value('resolved_connection_name', 'local');
        $connection_config = $this->settings_service->getNamedConnection($connection_name, true);
        
        if ($connection_config && $connection_config['type'] === 'local') {
            // Get path_prefix from connection config (CDN prefix)
            $path_prefix = $connection_config['config']['path_prefix'] ?? '';
        }
        
        // Second priority: Fall back to image_path_prefix parameter if no connection prefix
        if (empty($path_prefix)) {
            $path_prefix = $this->context->get_param('image_path_prefix', '');
        }
        
        if (!empty($path_prefix)) {
            // For full URLs (http/https), replace the entire URL with prefix + path
            if (str_starts_with($path_prefix, 'http://') || str_starts_with($path_prefix, 'https://')) {
                // Parse both URLs
                $prefix_parsed = parse_url($path_prefix);
                $base_parsed = parse_url($base_url);
                
                // Keep the path from base URL, but use prefix's scheme/host/port
                $result_parsed = $prefix_parsed;
                $result_parsed['path'] = rtrim($prefix_parsed['path'] ?? '', '/') . '/' . ltrim($base_parsed['path'] ?? '', '/');
                
                return $this->_rebuild_url($result_parsed);
            } else {
                // For relative prefixes, insert into URL path
                $path_prefix = '/' . trim($path_prefix, '/') . '/';
                
                // Insert prefix into URL construction
                $parsed_url = parse_url($base_url);
                $path = $parsed_url['path'] ?? '';
                $parsed_url['path'] = $path_prefix . ltrim($path, '/');
                
                return $this->_rebuild_url($parsed_url);
            }
        }
        
        return $base_url;
    }
    
    /**
     * Apply lazy loading filter to cached image (simplified version for cache use)
     * 
     * @param \Imagine\Image\ImageInterface $image Source image
     * @param string $filter_name Filter to apply
     * @return \Imagine\Image\ImageInterface|null Filtered image
     */
    private function _apply_lazy_filter_for_cache(\Imagine\Image\ImageInterface $image, string $filter_name): ?\Imagine\Image\ImageInterface
    {
        try {
            switch ($filter_name) {
                case 'lqip':
                    $filter = new \JCOGSDesign\JCOGSImagePro\Filters\Lqip();
                    return $filter->apply($image);
                    
                case 'dominant_color':
                    $filter = new \JCOGSDesign\JCOGSImagePro\Filters\DominantColor();
                    return $filter->apply($image);
                    
                default:
                    return null;
            }
        } catch (\Exception $e) {
            $this->utilities_service->debug_error('lazy_filter_cache_apply_error', $e);
            return null;
        }
    }
    
    /**
     * Build comprehensive Legacy-compatible template variables
     * 
     * @param string $cache_local_path Local cache path
     * @param string $cache_full_url Full cache URL
     * @return array Complete set of template variables
     */
    private function _build_legacy_compatible_variables(string $cache_local_path, string $cache_full_url): array
    {
        // Get source and processed image info
        $src_path = $this->context->get_param('src', '');
        $original_width = (int) $this->context->get_metadata_value('original_width', 0);
        $original_height = (int) $this->context->get_metadata_value('original_height', 0);
        $final_width = (int) $this->context->get_metadata_value('final_width', 0);
        $final_height = (int) $this->context->get_metadata_value('final_height', 0);
        
        // NOTE: Dimension recovery removed - now handled centrally by _ensure_template_variables_complete()
        // which runs at the start of OutputStage processing for ALL output types
        
        // Calculate aspect ratios
        $aspect_ratio_orig = $original_width > 0 ? ($original_height / $original_width) : 0;
        $aspect_ratio = $final_width > 0 ? ($final_height / $final_width) : 0;
        
        // Get file extensions
        $extension_orig = pathinfo($src_path, PATHINFO_EXTENSION);
        $extension = pathinfo($cache_local_path, PATHINFO_EXTENSION);
        
        // Get file names
        $name_orig = pathinfo($src_path, PATHINFO_FILENAME);
        $name = pathinfo($cache_local_path, PATHINFO_FILENAME);
        
        // Get filesizes using fast memory-based methods (avoiding filesystem access)
        
        // Try to get processed image filesize from memory object first (fastest method)
        $processed_filesize_bytes = 0;
        $processed_filesize = '';
        
        // First try: Get from processed image object in memory (most efficient)
        $processed_image = $this->context->get_processed_image();
        if ($processed_image && is_object($processed_image) && method_exists($processed_image, 'get')) {
            try {
                $save_as = $this->context->get_param('save_as', 'jpg');

                // Use quality options computed and stored during CacheStage
                $quality_options = $this->context->get_metadata_value('quality_options', ['quality' => 85]);

                // Debug message for save format and quality
                $quality_display = isset($quality_options['webp_lossless']) && $quality_options['webp_lossless'] ? 'lossless' : 
                    (isset($quality_options['avif_lossless']) && $quality_options['avif_lossless'] ? 'lossless' : 
                    $quality_options['quality']);
                $this->utilities_service->debug_message('saving_image_with_quality', [$save_as, $quality_display], false, 'detailed');
                
                $image_content = $processed_image->get($save_as, $quality_options);
                $processed_filesize_bytes = strlen($image_content);
                $processed_filesize = $this->image_utilities->format_file_size($processed_filesize_bytes);
            } catch (\Exception $e) {
                // Could not get processed file size from memory object
            }
        }
        
        // Second try: Get from cache metadata if available (also fast)
        if ($processed_filesize_bytes === 0) {
            $processed_filesize_bytes = $this->context->get_metadata_value('cache_size_bytes', 0);
            $processed_filesize = $processed_filesize_bytes > 0 ? $this->image_utilities->format_file_size($processed_filesize_bytes) : '';
        }
        
        // Get original image filesize from metadata (should already be stored)
        $original_filesize_bytes = $this->context->get_metadata_value('original_filesize_bytes', 0);
        $original_filesize = $original_filesize_bytes > 0 ? $this->image_utilities->format_file_size($original_filesize_bytes) : '';
        
        // If we still don't have original filesize, try from raw image data in memory
        if ($original_filesize_bytes <= 0) {
            $source_image_raw = $this->context->get_metadata_value('image_source_raw', '');
            if ($source_image_raw) {
                $original_filesize_bytes = strlen($source_image_raw);
                $original_filesize = $this->image_utilities->format_file_size($original_filesize_bytes);
            }
        }
        
        // Get lazy loading placeholder URL if applicable
        $lazy_image_url = '';
        if ($this->_get_lazy_loading_service()->is_lazy_loading_enabled($this->context)) {
            $mode = $this->_get_lazy_loading_service()->get_lazy_loading_mode($this->context);
            if ($mode && $this->_get_lazy_loading_service()->requires_placeholder($mode)) {
                $lazy_image_url = $this->_generate_placeholder_url($mode);
            }
        }
        
        // Get MIME type
        $mime_type = '';
        switch (strtolower($extension)) {
            case 'jpg':
            case 'jpeg':
                $mime_type = 'image/jpeg';
                break;
            case 'png':
                $mime_type = 'image/png';
                break;
            case 'webp':
                $mime_type = 'image/webp';
                break;
            case 'avif':
                $mime_type = 'image/avif';
                break;
            case 'gif':
                $mime_type = 'image/gif';
                break;
        }
        
        // Build attributes from parameters
        $attributes = [];
        
        // Collect attributes from tag parameters
        $tag_params = $this->context->get_tag_params();
        $attribute_params = ['class', 'style', 'alt', 'title', 'id'];
        foreach ($attribute_params as $attr) {
            if (isset($tag_params[$attr])) {
                $attributes[] = $attr . '="' . htmlspecialchars($tag_params[$attr]) . '"';
            }
        }
        
        // Handle data-* attributes
        foreach ($tag_params as $param_name => $param_value) {
            if (strpos($param_name, 'data-') === 0) {
                $attributes[] = $param_name . '="' . htmlspecialchars($param_value) . '"';
            }
        }
        
        $attributes_string = implode(' ', $attributes);
        
        // Get EE File Manager fields if source is a managed file
        // Pass tagdata so conditional execution check works (Issue #548 optimization)
        $tagdata = $this->context->get_tag_data() ?? '';
        $file_manager_fields = $this->utilities_service->get_file_manager_fields($src_path, $tagdata);
        
        // Build comprehensive variable set matching Legacy
        return [
            // Core processed image variables
            'made' => $cache_local_path,
            'made_url' => $cache_full_url,
            'made_with_prefix' => $cache_local_path, // For prefix support (future enhancement)
            'url' => $cache_local_path,              // Legacy compatibility
            'src' => $cache_local_path,              // Legacy compatibility
            
            // Dimensions - processed
            'width' => $final_width,
            'height' => $final_height,
            'aspect_ratio' => round($aspect_ratio, 6),
            
            // Dimensions - original
            'width_orig' => $original_width,
            'height_orig' => $original_height,
            'aspect_ratio_orig' => round($aspect_ratio_orig, 6),
            
            // File information - processed
            'extension' => $extension,
            'name' => $name,
            'path' => $cache_local_path,
            'type' => $extension,
            'filesize' => $processed_filesize,
            'filesize_bytes' => $processed_filesize_bytes,
            'mime_type' => $mime_type,
            
            // File information - original
            'extension_orig' => $extension_orig,
            'name_orig' => $name_orig,
            'path_orig' => $src_path,
            'orig' => $src_path,
            'orig_url' => $src_path, // May need URL conversion in future
            'type_orig' => $extension_orig,
            'filesize_orig' => $original_filesize,
            'filesize_bytes_orig' => $original_filesize_bytes,
            
            // Template attributes and metadata
            'attributes' => $attributes_string,
            'lazy_image' => $lazy_image_url,
            
            // EE File Manager fields (placeholder support)
            'img_credit' => $file_manager_fields['credit'] ?? '',
            'img_description' => $file_manager_fields['description'] ?? '',
            'img_location' => $file_manager_fields['location'] ?? '',
            'img_title' => $file_manager_fields['title'] ?? '',
            
            // Cache and processing metadata
            'cache_key' => $this->context->get_cache_key(),
            'using_cache' => $this->context->get_flag('using_cache_copy') ? 'yes' : 'no'
        ];
    }
    
    /**
     * Ensure template variables are complete with valid dimensions
     * 
     * This centralized method:
     * 1. Checks if template variables exist in metadata (from cache or CacheStage)
     * 2. Validates that critical dimensions (width/height) are present and non-zero
     * 3. If dimensions are missing, recovers them by reading the cached image file
     * 4. All values stored directly as individual metadata keys (no intermediate array)
     * 
     * This eliminates the architectural redundancy where template variables were stored
     * in both an array (computed_template_variables) and as individual metadata keys.
     * Now everything uses metadata directly.
     * 
     * @return void
     */
    private function _ensure_template_variables_complete(): void
    {
        // Get cache URL for building variables if needed
        $cache_url = $this->context->get_metadata_value('cache_url', '');
        if (empty($cache_url)) {
            $cache_url = $this->context->get_param('src', '');
        }
        
        // Check if dimensions already exist in metadata
        $width = (int) $this->context->get_metadata_value('width', 0);
        $height = (int) $this->context->get_metadata_value('height', 0);
        
        if ($width === 0 && $height === 0) {
            // Variables not built yet - build them now
            $cache_local_path = $cache_url;
            
            // Build full URL for template variables
            if (!str_starts_with($cache_url, 'http://') && !str_starts_with($cache_url, 'https://')) {
                $base_url = ee()->config->item('base_url') ?? '';
                $cache_full_url = $base_url . ltrim($cache_url, '/');
            } else {
                $cache_full_url = $cache_url;
            }
            
            $variables = $this->_build_legacy_compatible_variables($cache_local_path, $cache_full_url);
            $this->context->merge_metadata($variables);
            
            $this->utilities_service->debug_message('Built and merged template variables in OutputStage', [
                'width' => $variables['width'] ?? 0,
                'height' => $variables['height'] ?? 0
            ], false, 'detailed');
            
            // Update width/height for dimension recovery check below
            $width = (int) ($variables['width'] ?? 0);
            $height = (int) ($variables['height'] ?? 0);
        }
        
        // Validate dimensions are non-zero
        if ($width > 0 && $height > 0) {
            // Dimensions are valid, nothing more to do
            return;
        }
        
        // Dimensions are missing or zero - attempt recovery from cached file
        $this->utilities_service->debug_message('Recovering missing dimensions from cached file', [
            'cached_width' => $width,
            'cached_height' => $height
        ], false, 'detailed');
        
        $cache_local_path = $this->_convert_url_to_cache_path($cache_url);
        
        try {
            $connection_name = $this->context->get_metadata_value('resolved_connection_name', null);
            $read_path = $this->_get_read_path_for_connection($cache_local_path, $connection_name);
            
            // Use FilesystemService to check existence and read file
            $connection_name = $connection_name ?? 'default';
            
            if ($this->filesystem_service->exists($read_path, $connection_name)) {
                $image_content = $this->filesystem_service->read($read_path, $connection_name);
                
                // Load image with Imagine to get dimensions
                $imagine = new Imagine();
                $cached_image = $imagine->load($image_content);
                $size = $cached_image->getSize();
                
                // Update metadata with recovered dimensions
                $recovered_width = $size->getWidth();
                $recovered_height = $size->getHeight();
                $recovered_aspect = $recovered_width > 0 ? round($recovered_height / $recovered_width, 6) : 0;
                
                $this->context->set_metadata('width', $recovered_width);
                $this->context->set_metadata('height', $recovered_height);
                $this->context->set_metadata('aspect_ratio', $recovered_aspect);
                $this->context->set_metadata('final_width', $recovered_width);
                $this->context->set_metadata('final_height', $recovered_height);
                
                $this->utilities_service->debug_message('Dimensions recovered successfully', [
                    'width' => $recovered_width,
                    'height' => $recovered_height
                ], false, 'detailed');
                
                // CRITICAL: Update cache_log with corrected dimensions (self-healing)
                // Build complete variables array from metadata for cache_log update
                try {
                    $all_metadata = $this->context->get_metadata();
                    $cache_dir = dirname($cache_local_path);
                    $processing_time = $this->context->get_metadata_value('processing_time', 0.0);
                    $source_path = $this->context->get_metadata_value('source_path', '');
                    
                    $cache_log_updated = $this->cache_service->update_cache_log(
                        image_path: $cache_local_path,
                        processing_time: $processing_time,
                        vars: $all_metadata,
                        cache_dir: $cache_dir,
                        source_path: $source_path,
                        force_update: true,
                        using_cache_copy: true,
                        connection_name: $connection_name
                    );
                    
                    if ($cache_log_updated) {
                        $this->utilities_service->debug_message('Cache log updated with recovered dimensions', [
                            'cache_path' => $cache_local_path
                        ], false, 'detailed');
                    }
                } catch (\Exception $update_error) {
                    $this->utilities_service->debug_message('Failed to update cache log: ' . $update_error->getMessage());
                }
            }
        } catch (\Exception $e) {
            $this->utilities_service->debug_error('dimension_recovery_failed_in_ensure', $e);
        }
    }
    
    /**
     * Convert cache URL to filesystem path
     * 
     * @param string $cache_url Cache URL (local path)
     * @return string Filesystem path for the cache file
     */
    private function _convert_url_to_cache_path(string $cache_url): string
    {
        // Use shared utility method for proper URL to path conversion
        return Utilities::convert_url_to_relative_path($cache_url);
    }
    
    /**
     * Get read path for cloud adapter with prefix stripping
     * 
     * Cloud adapters (S3, R2, etc.) have a server_path prefix configured that's automatically
     * prepended to all operations. When reading files, we need to strip this prefix from paths
     * to avoid path doubling (e.g., cache/jcogs_img_pro/cache/jcogs_img_pro/file.jpg).
     * 
     * Files are written to the root of the adapter's prefix (e.g., just "filename.jpg" within
     * the "cache/jcogs_img_pro" prefix), so when reading we need to pass only the relative
     * filename without the prefix.
     * 
     * @param string $cache_url Full cache URL
     * @param string|null $connection_name Connection name to check type
     * @return string Path suitable for reading from the adapter
     */
    private function _get_read_path_for_connection(string $cache_url, ?string $connection_name = null): string
    {
        // Convert URL to basic path
        $path = $this->_convert_url_to_cache_path($cache_url);
        
        // If no connection specified, return full path (fallback for local)
        if (empty($connection_name)) {
            return $path;
        }
        
        // Get connection configuration using FilesystemService
        try {
            $connection = $this->filesystem_service->getConnectionConfig($connection_name);
            
            if (!$connection) {
                return $path; // Unknown connection, use full path
            }
            
            $type = $connection['type'] ?? 'local';
            
            // For cloud adapters, strip the adapter's server_path prefix
            if (in_array($type, ['s3', 'r2', 'gcs', 'dospaces', 'azure'])) {
                $server_path = $connection['config']['server_path'] ?? '';
                
                if (!empty($server_path)) {
                    // Normalize and strip prefix
                    // Example: "cache/jcogs_img_pro/file.jpg" → "file.jpg"
                    $prefix = trim($server_path, '/') . '/';
                    
                    if (str_starts_with($path, $prefix)) {
                        $path = substr($path, strlen($prefix));
                    }
                }
            }
            
            return $path;
            
        } catch (\Exception $e) {
            // On error, return full path as fallback
            return $path;
        }
    }
    
    /**
     * Determine what type of output to generate
     * 
     * @return string Output type
     */
    private function _determine_output_type(): string 
    {
        $tag_data = $this->context->get_tag_data();
        
        // Check if this is an ACT request first (highest priority)
        $validation_service = \JCOGSDesign\JCOGSImagePro\Service\ServiceCache::validation();
        if ($validation_service->is_act_processing()) {
            return 'act_image_serve';
        }
        
        // Check if this is a palette tag
        $called_by = $this->context->get_flag('called_by') ?: '';
        if ($called_by === 'Palette_Tag') {
            return 'palette_variables';
        }
        
        // Check for url_only parameter (and that it is not a pair tag - we ignore url_only if it is a pair tag)
        $url_only = $this->context->get_param('url_only', '');
        if ((strtolower($url_only) === 'yes' || strtolower($url_only) === 'y') && empty($tag_data)) {
            return 'url_only';
        }
        
        // If there's tag data, it's a pair tag
        if (!empty($tag_data)) {
            // Check create_tag parameter for pair tags (Legacy line 957: create_tag != 'y')
            $create_tag = $this->context->get_param('create_tag', '');
            if (substr(strtolower($create_tag), 0, 1) === 'n') {
                return 'pair_variables'; // Parse tagdata only, no IMG tag
            }
            return 'pair_variables';
        }
        
        // Check if single tag has output parameter
        // Single tag with output param should parse variables like pair tag
        $output = $this->context->get_param('output', '');
        if (!empty($output) && $output !== 'tag') {
            return 'pair_variables'; // Use pair_variables to parse the output content
        }
        
        // Check create_tag parameter for single tags
        $create_tag = $this->context->get_param('create_tag', '');
        if (substr(strtolower($create_tag), 0, 1) === 'n') {
            // create_tag = 'n' so return no output
            return 'no_output';
        }
        
        // Default to IMG tag (Legacy line 966: create_tag == 'y' or single tag with no create_tag)
        return 'img_tag';
    }
    
    /**
     * Ensure lazy placeholder exists, generate if missing (Legacy pattern)
     * 
     * This method replicates Legacy JcogsImage::_generate_lazy_placeholder_image() behavior:
     * - Build expected placeholder filename and path
     * - Check if placeholder exists in cache
     * - If not, load main cached image and generate placeholder using filters
     * - Return placeholder URL
     * 
     * @param string $filter_name Filter name (lqip, dominant_color)
     * @return string Placeholder URL or fallback data URL
     */
    private function _ensure_lazy_placeholder_exists(string $filter_name): string
    {
        try {
            // Skip placeholder generation for cached images loaded via fast path
            // If the image is from cache, the placeholder should have been generated during original processing
            // Check metadata first to see if we know what cached placeholder URL is
            $using_cache_copy = $this->context->get_flag('using_cache_copy');
            if ($using_cache_copy) {
                // Check metadata for cached placeholder URL first
                $metadata_key = $filter_name . '_url';
                $cached_placeholder_url = $this->context->get_metadata_value($metadata_key, '');
                
                if (!empty($cached_placeholder_url)) {
                    $this->utilities_service->debug_message('Using cached placeholder URL: ' . $filter_name, null, false, 'detailed');
                    return $cached_placeholder_url;
                }
                
                // Placeholder wasn't generated during original processing, go on to look for it elsewhere or generate it
                $this->utilities_service->debug_message('No cached placeholder found, using fallback: ' . $filter_name, null, false, 'detailed');
            }
            
            // Get main image cache info
            $cache_url = $this->context->get_metadata_value('cache_url', '');
            if (empty($cache_url)) {
                // No main cache URL available, use fallback
                return $this->_get_fallback_placeholder_url($filter_name);
            }
            
            // Build expected placeholder path and URL
            $cache_path_info = pathinfo($cache_url);
            $placeholder_filename = $cache_path_info['filename'] . '_' . $filter_name . '.' . $cache_path_info['extension'];
            $placeholder_url = $cache_path_info['dirname'] . '/' . $placeholder_filename;
            
            // Check cache_log for placeholder existence
            $cache_management_service = $this->cache_service;
            // Get the likely location for the file in cache
            $placeholder_cache_path = $this->_convert_url_to_cache_path($placeholder_url);
            
            // Try to load placeholder from cache_log
            $placeholder_cache_data = $cache_management_service->load_cached_image_data($placeholder_cache_path);
            
            if ($placeholder_cache_data && !empty($placeholder_cache_data['cache_url'])) {
                // Found file in cache_log, return URL
                $this->utilities_service->debug_message('found_lazy_placeholder_cache_log', [$placeholder_filename], false, 'detailed');
                return $placeholder_cache_data['cache_url'];
            }
            
            // Not found via cache_log, check cache directly in case it exists there
            $filesystem = $this->filesystem_service;
            if ($filesystem->exists($placeholder_cache_path)) {
                // Placeholder exists on disk but not in cache_log, return URL
                $this->utilities_service->debug_message('found_lazy_placeholder_filesystem', [$placeholder_filename], false, 'detailed');
                return $placeholder_url;
            }
            
            // Placeholder doesn't exist, generate it on-demand
            $this->utilities_service->debug_message('generating_missing_lazy_placeholder', [$placeholder_filename], false, 'detailed');
            
            // Load the main cached image - check cache_log first, then filesystem
            $main_cache_path = $this->_convert_url_to_cache_path($cache_url);
            $main_cache_data = $cache_management_service->load_cached_image_data($main_cache_path);
            
            // Determine connection for reading
            $connection_name = $this->context->get_metadata_value('resolved_connection_name', 'local');
            
            // For cloud adapters, we should skip the filesystem existence check since the path
            // isn't a local filesystem path - just try to read it and catch exceptions
            $file_exists = false;
            if ($connection_name === 'local') {
                // Local: Use normal exists check
                $file_exists = $main_cache_data || $filesystem->exists($main_cache_path, $connection_name);
            } else {
                // Cloud: Trust cache_log data, or we'll verify when we try to read
                $file_exists = (bool)$main_cache_data;
            }
            
            if (!$file_exists) {
                // Main image doesn't exist in cache_log, use fallback
                $this->utilities_service->debug_message('main_cached_image_not_found', [$main_cache_path]);
                return $this->_get_fallback_placeholder_url($filter_name);
            }
            
            // Load the cached image using Imagine
            // Use FilesystemService to read file content for both local and cloud adapters (Imagine cannot talk to the cloud directly)
            try {
                $imagine = new Imagine();
                $image_content = $filesystem->read($main_cache_path, $connection_name);
                $cached_image = $imagine->load($image_content);
            } catch (\Exception $e) {
                // Failed to read or load image
                $this->utilities_service->debug_message('failed_to_load_cached_image', [$main_cache_path, $e->getMessage()]);
                return $this->_get_fallback_placeholder_url($filter_name);
            }
            
            // Apply the appropriate filter to generate placeholder
            $placeholder_image = $this->_apply_lazy_filter_for_cache($cached_image, $filter_name);
            
            if (!$placeholder_image) {
                // Filter failed, use fallback
                return $this->_get_fallback_placeholder_url($filter_name);
            }
            
            // Save the placeholder image
            $quality = $this->_get_lazy_image_quality($filter_name, $cache_path_info['extension']);
            $success = $filesystem->writeImage($placeholder_image, $placeholder_cache_path, [
                'quality' => $quality
            ]);
            
            if ($success) {
                $this->utilities_service->debug_message('successfully_generated_ondemand_placeholder', [$placeholder_filename], false, 'detailed');
                
                // Update cache log for the placeholder
                $this->_update_placeholder_cache_log($placeholder_cache_path, $filter_name);
                
                return $placeholder_url;
            } else {
                $this->utilities_service->debug_message('failed_save_ondemand_placeholder', [$placeholder_filename]);
                return $this->_get_fallback_placeholder_url($filter_name);
            }
            
        } catch (\Exception $e) {
            $this->utilities_service->debug_error('lazy_placeholder_ensure_error', $e);
            return $this->_get_fallback_placeholder_url($filter_name);
        }
    }

    /**
     * Generate action link URL if action_link parameter is enabled
     * 
     * Migrated from Legacy JcogsImage::_generate_action_link()
     * Creates EE Action URLs that serve images directly via act_originated_image method
     * 
     * @param string $image_url Current image URL
     * @param string $what Type indicator (e.g., 'img', 'url')
     * @return string Action URL or original URL if action links disabled
     */
    private function _generate_action_link(string $image_url, string $what = 'img'): string
    {
        // Check if action links are enabled via parameter or global setting
        $action_link_param = $this->context->get_param('action_link', 'auto');
        $global_action_links = $this->settings_service->get('img_cp_action_links', 'n');
        
        // Logic: 
        // - If action_link="y" → force enable
        // - If action_link="n" → force disable  
        // - If action_link not set (auto) → use global setting
        $action_links_enabled = false;
        if (strtolower(substr($action_link_param, 0, 1)) === 'y') {
            $action_links_enabled = true;
        } elseif (strtolower(substr($action_link_param, 0, 1)) === 'n') {
            $action_links_enabled = false;
        } else {
            // Use global setting when parameter is not explicitly set
            $action_links_enabled = (strtolower(substr($global_action_links, 0, 1)) === 'y');
        }
        
        if (!$action_links_enabled) {
            $this->utilities_service->debug_message('Action links disabled', null, false, 'detailed');
            return $image_url;
        }
        
        // Debug: Log action link generation when enabled
        $this->utilities_service->debug_message('Generating action link for: ' . $image_url . ' (param: ' . $action_link_param . ', global: ' . $global_action_links . ')', null, false, 'detailed');
        
        // Get ORIGINAL context parameters for ACT packet (not current parameters which may have been modified during processing)
        // This ensures consistent cache keys between regular tag processing and ACT regeneration
        $all_params = $this->context->get_original_tag_params();

        // Set specific ACT parameters
        $all_params['action_link'] = 'no'; // Prevent recursive action link generation
        $all_params['act_what'] = $what;
        $all_params['act_path'] = $image_url;
        $all_params['url_only'] = 'yes'; // Force URL only mode for ACT processing
        
        // Ensure color parameters are properly formatted
        if (isset($all_params['bg_color'])) {
            // Use validation service to ensure proper color format
            $all_params['bg_color'] = $this->validation_service->validate_color_string($all_params['bg_color']);
        }
        
        // Build ACT packet
        $act_packet = base64_encode(json_encode($all_params));
        
        if (empty($act_packet)) {
            $this->utilities_service->debug_message('Action link generation failed: JSON encoding error', null, false, 'detailed');
            return $image_url;
        }
        
        // Get action ID for act_originated_image
        $act_id = $this->utilities_service->get_action_id('act_originated_image');
        if (!$act_id) {
            $this->utilities_service->debug_message('Action link generation failed: No action ID found for act_originated_image', null, false, 'detailed');
            return $image_url;
        }
        
        // Debug: Log successful action ID lookup
        $this->utilities_service->debug_message('Action ID found: ' . $act_id, null, false, 'detailed');
        
        // Build action URL
        $append_path = $this->settings_service->get('img_cp_append_path_to_action_links', 'n');
        if (strtolower(substr($append_path, 0, 1)) === 'y') {
            $action_url = sprintf(
                '%s?ACT=%s&act_packet=%s&path=%s',
                ee()->config->item('site_url'),
                $act_id,
                $act_packet,
                urlencode($image_url)
            );
        } else {
            $action_url = sprintf(
                '%s?ACT=%s&act_packet=%s',
                ee()->config->item('site_url'),
                $act_id,
                $act_packet
            );
        }
        
        $this->utilities_service->debug_message('Generated action link: ' . $action_url, null, false, 'detailed');
        return $action_url;
    }
    
    /**
     * Generate base64 data from processed image
     * 
     * @return string|null Base64 data URI or null if not available
     */
    private function _generate_base64_from_processed_image(): ?string
    {
        // Try to get from cache first if using cached copy
        if ($this->context->get_flag('using_cache_copy')) {
            // Get cache URL from context (this is the full public URL for the cached image)
            $cache_url = $this->context->get_metadata_value('cache_url', '');
            if ($cache_url) {
                try {
                    $filesystem = $this->filesystem_service;
                    
                    // Get the connection name that was used to cache this image
                    $connection_name = $this->context->get_metadata_value('resolved_connection_name', null);
                    if (!$connection_name) {
                        // Fallback to default connection if not stored in metadata
                        $connection_name = $this->settings_service->get_default_connection_name();
                    }
                    
                    // Convert cache URL to read path, stripping adapter prefix for cloud connections
                    $cache_path = $this->_get_read_path_for_connection($cache_url, $connection_name);
                    
                    // Read image content from the correct connection
                    $image_content = $filesystem->read($cache_path, $connection_name);
                    if ($image_content) {
                        $save_as = $this->context->get_param('save_as', 'jpg');
                        $mime_type = $this->_get_mime_type($save_as);
                        return 'data:' . $mime_type . ';base64,' . base64_encode($image_content);
                    }
                } catch (\Exception $e) {
                    $this->utilities_service->debug_log('Could not read cached image for base64: ' . $e->getMessage());
                }
            }
        }
        
        // Try to get from processed image object
        $processed_image = $this->context->get_processed_image();
        if ($processed_image && is_object($processed_image) && method_exists($processed_image, 'get')) {
            try {
                $save_as = $this->context->get_param('save_as', 'jpg');

                // Use quality options computed and stored during CacheStage
                $quality_options = $this->context->get_metadata_value('quality_options', ['quality' => 85]);

                $image_content = $processed_image->get($save_as, $quality_options);
                $mime_type = $this->_get_mime_type($save_as);
                return 'data:' . $mime_type . ';base64,' . base64_encode($image_content);
            } catch (\Exception $e) {
                $this->utilities_service->debug_log('Could not generate base64 from processed image: ' . $e->getMessage());
            }
        }
        
        return null;
    }
    
    /**
     * Generate base64 data from source image
     * 
     * @return string|null Base64 data URI or null if not available
     */
    private function _generate_base64_from_source_image(): ?string
    {
        // Only generate for non-cached images (like legacy)
        if ($this->context->get_flag('using_cache_copy')) {
            return null;
        }
        
        // Try to get from raw source image data
        $source_image_raw = $this->context->get_metadata_value('image_source_raw', '');
        if ($source_image_raw) {
            try {
                // Detect MIME type from raw data or use original extension
                $src_path = $this->context->get_param('src', '');
                $extension = pathinfo($src_path, PATHINFO_EXTENSION);
                $mime_type = $this->_get_mime_type($extension);
                
                return 'data:' . $mime_type . ';base64,' . base64_encode($source_image_raw);
            } catch (\Exception $e) {
                $this->utilities_service->debug_log('Could not generate base64_orig from source image: ' . $e->getMessage());
            }
        }
        
        return null;
    }
    
    /**
     * Generate base64 variables on-demand (replicates legacy line 635)
     * 
     * @param string $template_content
     * @param array $variables Variables array to modify by reference
     */
    private function _generate_base64_variables_on_demand(string $template_content, array &$variables): void
    {
        // Check if base64 is needed in the template content
        if (stripos($template_content, '{base64}') !== false && !isset($variables['base64'])) {
            $base64_data = $this->_generate_base64_from_processed_image();
            if ($base64_data) {
                $variables['base64'] = $base64_data;
                $this->utilities_service->debug_message('Generated base64 data for processed image');
            }
        }
        
        // Check if base64_orig is needed in the template content
        if (stripos($template_content, '{base64_orig}') !== false && !isset($variables['base64_orig'])) {
            $base64_orig_data = $this->_generate_base64_from_source_image();
            if ($base64_orig_data) {
                $variables['base64_orig'] = $base64_orig_data;
                $this->utilities_service->debug_message('Generated base64_orig data for source image');
            }
        }
    }
    
    /**
     * Generate HTML IMG tag output
     * 
     * @return string IMG tag HTML
     */
    private function _generate_img_tag(): string 
    {
        $cache_url = $this->context->get_metadata_value('cache_url', '');
        if (empty($cache_url)) {
            // Fallback to original source for special cases
            $cache_url = $this->context->get_param('src', '');

            // If it all goes wrong ... :(
            if (empty($cache_url)) {
                $cache_url = '/path/to/missing/image.jpg'; // Placeholder to prevent broken HTML
            }
        }

        // Apply always_output_full_urls setting for local connections
        $cache_url = $this->_apply_full_url_setting($cache_url);

        // Apply image path prefix if specified
        $cache_url = $this->_apply_image_path_prefix($cache_url);

        // NOTE: Action link generation moved to after lazy loading processing

        // Get image dimensions directly from metadata
        // All dimensions are now stored as individual metadata keys (no intermediate array)
        $image_dimensions = [
            'width' => (int) $this->context->get_metadata_value('width', 0),
            'height' => (int) $this->context->get_metadata_value('height', 0)
        ];

        // Generate responsive image data if enabled
        $responsive_data = [];
        if ($this->responsive_service->is_responsive_enabled($this->context)) {
            $base_width = (int) $this->context->get_metadata_value('final_width', 0);
            $max_width = (int) $this->context->get_metadata_value('original_width', $base_width);

            // Check if allow_scale_larger parameter is enabled
            $allow_scale_larger_param = strtolower(trim($this->context->get_param('allow_scale_larger', 'no')));
            $allow_scale_larger_bool = in_array($allow_scale_larger_param, ['yes', 'y', 'true', '1'], true);
            
            $this->utilities_service->debug_message(
                'debug_output_stage_img_tag',
                sprintf('OutputStage (_generate_img_tag): base_width=%d, max_width=%d, allow_scale_larger_param="%s", allow_scale_larger_bool=%s',
                    $base_width, $max_width, $allow_scale_larger_param, $allow_scale_larger_bool ? 'true' : 'false'
                ),
                false,
                'detailed'
            );

            // Generate variant information for responsive images
            $variants = $this->responsive_service->generate_variant_info(
                $this->context, 
                $base_width, 
                $max_width,
                $allow_scale_larger_bool
            );
            
            // Generate srcset and sizes attributes
            if (!empty($variants)) {
                // Extract base URL and construct srcset
                $base_url = dirname($cache_url) . '/';
                
                // Apply image path prefix to responsive base URL as well
                $base_url = $this->_apply_image_path_prefix($base_url);

                $responsive_data['srcset'] = $this->responsive_service->generate_srcset_attribute(
                    $variants,
                    $base_url,
                    $cache_url,
                    $base_width
                );
                $responsive_data['sizes'] = $this->responsive_service->generate_sizes_attribute(
                    $this->context,
                    $variants,
                    $base_width
                );
            }
            
            $this->utilities_service->debug_message('debug_responsive_images_generated', [count($variants)], false, 'detailed');
        }

        // Handle lazy loading placeholder if needed
        $placeholder_url = '';
        if ($this->_get_lazy_loading_service()->is_lazy_loading_enabled($this->context)) {
            $mode = $this->_get_lazy_loading_service()->get_lazy_loading_mode($this->context);

            if ($mode && $this->_get_lazy_loading_service()->requires_placeholder($mode)) {
                // Generate placeholder URL based on mode
                $placeholder_url = $this->_generate_placeholder_url($mode);
                
                // Apply image path prefix to placeholder URL if it's not a data URL
                if (!str_starts_with($placeholder_url, 'data:')) {
                    $placeholder_url = $this->_apply_image_path_prefix($placeholder_url);
                }
            }
            
            $this->utilities_service->debug_message('Lazy loading enabled: ' . ($mode ?? 'html5'), null, false, 'detailed');
        }

        // Use OutputGenerationService to create the complete HTML output
        $img_tag = $this->output_generation_service->generate_html_output(
            $this->context,
            $cache_url,
            $image_dimensions,
            $responsive_data,
            $placeholder_url
        );
        
        $this->utilities_service->debug_message('Output IMG tag generated', null, false, 'detailed');
        
        return $img_tag;
    }
    
    /**
     * Generate pair tag variables output
     * 
     * @return string Variables output for pair tags
     */
    private function _generate_pair_variables(): string
    {
        // Get tag data - either from actual pair tag or from output parameter (legacy line 958)
        $tag_data = $this->context->get_tag_data();
        if (empty($tag_data)) {
            // Check if this is a single tag with output parameter
            $tag_data = $this->context->get_param('output', '');
        }
        
        if (empty($tag_data)) {
            return '';
        }
        
        // Get cache URL and build template variables
        $cache_url = $this->context->get_metadata_value('cache_url', '');
        if (empty($cache_url)) {
            $cache_url = $this->context->get_param('src', '');
        }
        
        // Apply always_output_full_urls setting for local connections
        $cache_url = $this->_apply_full_url_setting($cache_url);
        
        // Apply image path prefix if specified (for CDN or custom paths)
        $cache_url = $this->_apply_image_path_prefix($cache_url);
        
        // Build template variables array from metadata on-demand
        // All values are stored as individual metadata keys, we build the array when needed for template parsing
        $variables = $this->context->get_metadata();
        
        // Ensure URL variables are properly set if missing
        if (empty($variables['made_url'])) {
            $cache_local_path = $cache_url;
            
            // For cache_full_url, always ensure it's a complete URL
            if (!str_starts_with($cache_url, 'http://') && !str_starts_with($cache_url, 'https://')) {
                $base_url = ee()->config->item('base_url') ?? '';
                if (preg_match('/^https?:\/[^\/]/', $base_url)) {
                    $base_url = preg_replace('/^(https?):\/([^\/])/', '$1://$2', $base_url);
                }
                $cache_full_url = $base_url . ltrim($cache_url, '/');
            } else {
                $cache_full_url = $cache_url;
            }
            
            // Build and merge missing variables
            $built_variables = $this->_build_legacy_compatible_variables($cache_local_path, $cache_full_url);
            $this->context->merge_metadata($built_variables);
            $variables = $this->context->get_metadata();
            
            $this->utilities_service->debug_message('Built missing URL variables in _generate_pair_variables', null, false, 'detailed');
        }
        
        // Generate responsive image variables if enabled - use loaded template variables
        $responsive_variables = [];
        if ($this->responsive_service->is_responsive_enabled($this->context)) {
            // Use loaded template variables for dimensions
            $base_width = (int) ($variables['width'] ?? 0);
            $max_width = (int) ($variables['width_orig'] ?? $base_width);
            
            // Check if allow_scale_larger parameter is enabled
            $allow_scale_larger_param = strtolower(trim($this->context->get_param('allow_scale_larger', 'no')));
            $allow_scale_larger_bool = in_array($allow_scale_larger_param, ['yes', 'y', 'true', '1'], true);
            
            $this->utilities_service->debug_message(
                'debug_output_stage_pair_tag',
                sprintf('OutputStage (_generate_pair_tag_output): base_width=%d, max_width=%d, allow_scale_larger_param="%s", allow_scale_larger_bool=%s',
                    $base_width, $max_width, $allow_scale_larger_param, $allow_scale_larger_bool ? 'true' : 'false'
                ),
                false,
                'detailed'
            );
            
            $variants = $this->responsive_service->generate_variant_info(
                $this->context, 
                $base_width, 
                $max_width,
                $allow_scale_larger_bool
            );
            
            if (!empty($variants)) {
                $base_url = dirname($cache_url) . '/';
                
                // Apply image path prefix to responsive base URL as well
                $base_url = $this->_apply_image_path_prefix($base_url);
                
                $responsive_variables['srcset'] = $this->responsive_service->generate_srcset_attribute(
                    $variants,
                    $base_url,
                    $cache_url,
                    $base_width
                );
                $responsive_variables['sizes'] = $this->responsive_service->generate_sizes_attribute(
                    $this->context,
                    $variants,
                    $base_width
                );
            }
        }
        
        // Generate lazy loading variables if enabled
        $lazy_variables = [];
        if ($this->_get_lazy_loading_service()->is_lazy_loading_enabled($this->context)) {
            $mode = $this->_get_lazy_loading_service()->get_lazy_loading_mode($this->context);
            $lazy_variables['lazy_mode'] = $mode ?? 'html5';
            $lazy_variables['is_lazy'] = 'yes';
            
            if ($mode && $this->_get_lazy_loading_service()->requires_placeholder($mode)) {
                $lazy_placeholder = $this->_generate_placeholder_url($mode);
                
                // Apply image path prefix to placeholder URL if it's not a data URL
                if (!str_starts_with($lazy_placeholder, 'data:')) {
                    $lazy_placeholder = $this->_apply_image_path_prefix($lazy_placeholder);
                }
                
                $lazy_variables['lazy_image'] = $lazy_placeholder;
            }
        } else {
            $lazy_variables['lazy_mode'] = 'none';
            $lazy_variables['is_lazy'] = 'no';
        }
        
        // Merge in responsive image variables
        $variables = array_merge($variables, $responsive_variables);
        
        // Merge in lazy loading variables
        $variables = array_merge($variables, $lazy_variables);
        
        // Generate base64 variables on-demand (like legacy line 635)
        $this->_generate_base64_variables_on_demand($tag_data, $variables);

        // Parse template content with variables using EE's template parser (Legacy pattern)
        // This mirrors Legacy line 950-951: ee()->TMPL->parse_variables($this->params->output, $this->vars)
        // and line 955-956: ee()->TMPL->parse_variables($this->tagdata, $this->vars)
        
        // Format variables for EE template parser - it expects a nested array structure
        $template_vars = [$variables]; // EE expects array of variable sets
        
        // Use EE's template parser for proper variable processing (supports conditionals, modifiers, etc)
        if (!empty(ee()->TMPL)) {
            // Standard template processing
            $output = ee()->TMPL->parse_variables($tag_data, $template_vars);
        } else {
            // ACT/direct processing fallback - use template service
            $output = ee('Template')->parse_variables($tag_data, $template_vars);
        }
        
        $this->utilities_service->debug_log('debug_output_pair_variables_generated', strlen($output), false, 'detailed');
        
        return $output;
    }
    
    /**
     * Generate palette tag variables output
     * 
    * @return string Variables output for palette tags
     */
    private function _generate_palette_variables(): string 
    {
        $tag_data = $this->context->get_tag_data();
        if (empty($tag_data)) {
            return '';
        }
        
        // Get palette data from context metadata (set by ProcessImageStage)
        $palette_data = $this->context->get_metadata_value('palette_data', []);
        if (empty($palette_data)) {
            $this->utilities_service->debug_log('No palette data found in context metadata');
            return $tag_data; // Return unparsed template
        }
        
        // Build template variables for palette
        $variables = [
            'dominant_color' => $palette_data['dominant_color'] ?? 'rgb(128,128,128)',
        ];
        
        // Parse template content with variables
        $output = $tag_data;
        
        // Replace simple variables first
        foreach ($variables as $key => $value) {
            $output = str_replace('{' . $key . '}', $value, $output);
        }
        
        // Handle {colors} loop if present
        if (isset($palette_data['colors']) && is_array($palette_data['colors'])) {
            $colors_pattern = '/\{colors\}(.*?)\{\/colors\}/s';
            if (preg_match($colors_pattern, $output, $matches)) {
                $colors_template = $matches[1];
                $colors_output = '';
                
                foreach ($palette_data['colors'] as $color_data) {
                    $color_row = $colors_template;
                    $color_row = str_replace('{color}', $color_data['color'], $color_row);
                    $color_row = str_replace('{rank}', $color_data['rank'], $color_row);
                    $colors_output .= $color_row;
                }
                
                $output = str_replace($matches[0], $colors_output, $output);
            }
        }
        
        $this->utilities_service->debug_log('debug_output_palette_variables_generated', strlen($output), false, 'detailed');
        
        return $output;
    }
    
    /**
     * Generate placeholder URL for lazy loading, ensuring placeholder exists
     * 
     * Replicates Legacy _generate_lazy_placeholder_image behavior:
     * 1. Check if lazy placeholder exists in cache
     * 2. If not, generate it on-demand
     * 3. Return the placeholder URL
     * 
     * This is critical for cached images where lazy placeholders weren't generated
     * during the current processing cycle.
     * 
     * @param string $mode Lazy loading mode (lqip, dominant_color, etc.)
     * @return string Placeholder URL
     */
    private function _generate_placeholder_url(string $mode): string
    {
        // Remove 'js_' prefix if present (like Legacy does)
        $filter_name = str_replace('js_', '', strtolower($mode));
        
        switch ($filter_name) {
            case 'lqip':
                // First check metadata (in case placeholder was generated during this processing)
                $metadata = $this->context->get_metadata();
                if (!empty($metadata['lqip_url'])) {
                    // Note: Prefix will be applied by caller to avoid double-application
                    return $metadata['lqip_url'];
                }
                
                // Check if LQIP placeholder exists in cache and generate if needed
                // Note: Prefix will be applied by caller to avoid double-application
                return $this->_ensure_lazy_placeholder_exists('lqip');

            case 'dominant_color':
                // First check metadata (in case placeholder was generated during this processing)
                $metadata = $this->context->get_metadata();
                if (!empty($metadata['dominant_color_url'])) {
                    // Note: Prefix will be applied by caller to avoid double-application
                    return $metadata['dominant_color_url'];
                }
                
                // Check if dominant color placeholder exists in cache and generate if needed
                // Note: Prefix will be applied by caller to avoid double-application
                return $this->_ensure_lazy_placeholder_exists('dominant_color');

            default:
                return '';
        }
    }
    
    /**
     * Generate URL-only output (for url_only="yes" parameter)
     * 
     * @return string Image URL (with action link if enabled)
     */
    private function _generate_url_only(): string 
    {
        $cache_url = $this->context->get_metadata_value('cache_url', '');
        if (empty($cache_url)) {
            $cache_url = $this->context->get_param('src', '');
        }
        
        // Apply always_output_full_urls setting for local connections
        $cache_url = $this->_apply_full_url_setting($cache_url);
        
        // Apply image path prefix if specified
        $cache_url = $this->_apply_image_path_prefix($cache_url);
        
        // Apply action link generation if enabled
        $final_url = $this->_generate_action_link($cache_url, 'url');

        $this->utilities_service->debug_log('debug_output_url_only_generated', $final_url, false, 'detailed');
        
        return $final_url;
    }
    
    /**
     * Get fallback placeholder URL for when generation fails
     * 
     * @param string $filter_name Filter name
     * @return string Fallback data URL
     */
    private function _get_fallback_placeholder_url(string $filter_name): string
    {
        switch ($filter_name) {
            case 'lqip':
                // Fallback to simple 1x1 transparent pixel
                return 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
                
            case 'dominant_color':
                // Fallback to simple colored placeholder
                return 'data:image/svg+xml;base64,' . base64_encode(
                    '<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"><rect width="100%" height="100%" fill="#f0f0f0"/></svg>'
                );
                
            default:
                return '';
        }
    }
    
    /**
     * Get quality setting for lazy images
     * 
     * @param string $filter_name Filter name
     * @param string $extension File extension
     * @return int Quality setting
     */
    private function _get_lazy_image_quality(string $filter_name, string $extension): int
    {
        // Use lower quality for LQIP to reduce file size
        if ($filter_name === 'lqip') {
            return $extension === 'jpg' ? 50 : 85;
        }
        
        // Use high quality for dominant color (simple image)
        if ($filter_name === 'dominant_color') {
            return 100;
        }
        
        return 85;
    }
    
    /**
     * Get lazy loading service (lazy initialisation to prevent circular dependency)
     * 
     * @return mixed LazyLoadingService instance
     */
    private function _get_lazy_loading_service()
    {
        return \JCOGSDesign\JCOGSImagePro\Service\ServiceCache::lazy_loading();
    }
    
    /**
     * Get MIME type for file extension
     * 
     * @param string $extension
     * @return string MIME type
     */
    private function _get_mime_type(string $extension): string
    {
        switch (strtolower($extension)) {
            case 'jpg':
            case 'jpeg':
                return 'image/jpeg';
            case 'png':
                return 'image/png';
            case 'webp':
                return 'image/webp';
            case 'avif':
                return 'image/avif';
            case 'gif':
                return 'image/gif';
            case 'svg':
                return 'image/svg+xml';
            default:
                return 'image/jpeg'; // Default fallback
        }
    }
    
    /**
     * Rebuild URL from parsed components
     * 
     * @param array $parsed_url Parsed URL components
     * @return string Rebuilt URL
     */
    private function _rebuild_url(array $parsed_url): string
    {
        $url = '';
        
        if (!empty($parsed_url['scheme'])) {
            $url .= $parsed_url['scheme'] . '://';
        }
        
        if (!empty($parsed_url['host'])) {
            if (!empty($parsed_url['user'])) {
                $url .= $parsed_url['user'];
                if (!empty($parsed_url['pass'])) {
                    $url .= ':' . $parsed_url['pass'];
                }
                $url .= '@';
            }
            $url .= $parsed_url['host'];
            
            if (!empty($parsed_url['port'])) {
                $url .= ':' . $parsed_url['port'];
            }
        }
        
        if (!empty($parsed_url['path'])) {
            $url .= $parsed_url['path'];
        }
        
        if (!empty($parsed_url['query'])) {
            $url .= '?' . $parsed_url['query'];
        }
        
        if (!empty($parsed_url['fragment'])) {
            $url .= '#' . $parsed_url['fragment'];
        }
        
        return $url;
    }
    
    /**
     * Serve raw image data for ACT requests (mirrors Legacy _send_act_link_image)
     * 
     * This method serves the processed image directly with appropriate headers
     * and terminates execution. Called when ACT processing flag is detected.
     * 
    * @return void Execution terminates after serving image
     */
    private function _serve_act_image(): void
    {
        $this->utilities_service->debug_message('ACT: OutputStage serving raw image data');
        $this->utilities_service->debug_log('JCOGS ACT: OutputStage serving raw image data');
        
        // Get the processed image path from context metadata
        $cache_url = $this->context->get_metadata_value('cache_url', '');
        if (empty($cache_url)) {
            $this->utilities_service->debug_message('ACT: No cache URL available for image serving');
            $this->utilities_service->debug_log('JCOGS ACT: No cache URL in context metadata');
            header($_SERVER["SERVER_PROTOCOL"] . " 404 Not Found");
            echo "Image not found";
            exit;
        }
        
        // Get the connection name that was used to cache this image
        $connection_name = $this->context->get_metadata_value('resolved_connection_name', null);
        if (!$connection_name) {
            // Fallback to default connection if not stored in metadata
            $connection_name = $this->settings_service->get_default_connection_name();
        }
        
        // Convert cache URL to read path, stripping adapter prefix for cloud connections
        $cache_path = $this->_get_read_path_for_connection($cache_url, $connection_name);
        
        $this->utilities_service->debug_log('ACT: Converting cache URL to path - URL: ' . $cache_url . ' -> Path: ' . $cache_path . ' (Connection: ' . $connection_name . ')');
        
        // Try to read the image data using filesystem service with the connection name
        $image_raw = $this->filesystem_service->read($cache_path, $connection_name);
        
        if (empty($image_raw)) {
            $this->utilities_service->debug_message('ACT: Failed to read image data from path: ' . $cache_path . ' (URL: ' . $cache_url . ')');
            $this->utilities_service->debug_log('JCOGS ACT: Failed to read image from path: ' . $cache_path);
            header($_SERVER["SERVER_PROTOCOL"] . " 404 Not Found");
            echo "Image file not accessible";
            exit;
        }
        
        $this->utilities_service->debug_message('ACT: Successfully read image data, size: ' . strlen($image_raw) . ' bytes');
        $this->utilities_service->debug_log('JCOGS ACT: Image data read successfully, size: ' . strlen($image_raw));
        
        // Get image size
        $image_size = strlen($image_raw);
        
        // Set Content-Type header based on file extension (mirrors Legacy)
        $this->_set_act_content_type_header($cache_url);
        
        // Clear validation service parameters (mirrors Legacy clear_params)
        $validation_service = \JCOGSDesign\JCOGSImagePro\Service\ServiceCache::validation();
        $validation_service->clear_params();
        
        // Send Content-Length header and image data (mirrors Legacy)
        header('Content-Length: ' . $image_size);
        echo $image_raw;
        exit();
    }
    
    /**
     * Set Content-Type header for ACT image serving (mirrors Legacy switch statement)
     * 
     * @param string $image_path Path to the image file
     * @return void
     */
    private function _set_act_content_type_header(string $image_path): void
    {
        $extension = strtolower(pathinfo($image_path, PATHINFO_EXTENSION));
        
        $this->utilities_service->debug_message('ACT: Setting Content-Type for extension: ' . $extension);
        
        switch ($extension) {
            case 'avif':
                header('Content-Type: image/avif');
                break;
            case 'bmp':
                header('Content-Type: image/bmp');
                break;
            case 'gif':
                header('Content-Type: image/gif');
                break;
            case 'jpg':
            case 'jpeg':
                header('Content-Type: image/jpeg');
                break;
            case 'png':
                header('Content-Type: image/png');
                break;
            case 'svg':
                header('Content-Type: image/svg+xml');
                break;
            case 'tiff':
                header('Content-Type: image/tiff');
                break;
            case 'webp':
                header('Content-Type: image/webp');
                break;
            default:
                header($_SERVER["SERVER_PROTOCOL"] . " 400 Bad Request");
                echo "Unsupported file type.";
                exit;
        }
    }
    
    /**
     * Check if stage should be skipped
     * 
     * @return bool
     */
    public function should_skip(Context $context): bool 
    {
        return $context->has_critical_error();
    }
    
    /**
     * Apply full URL setting based on named connection configuration
     * 
     * Checks the always_output_full_urls setting for the current connection.
     * If set to 'n', ensures URL is relative path. If set to 'y', converts to full URL.
     * 
     * @param string $url Current URL (may be relative or full)
     * @return string Adjusted URL based on connection settings
     */
    private function _apply_full_url_setting(string $url): string
    {
        // Get the current connection name
        $connection_name = $this->context->get_metadata_value('resolved_connection_name', 'local');
        
        // Get connection configuration
        $connection_config = $this->settings_service->getNamedConnection($connection_name, true);
        
        if (!$connection_config || $connection_config['type'] !== 'local') {
            // Not a local connection, return URL as-is
            return $url;
        }
        
        // Check always_output_full_urls setting (default to 'n' if not set)
        $always_full_urls = $connection_config['config']['always_output_full_urls'] ?? 'n';
        
        if (substr(strtolower($always_full_urls), 0, 1) === 'y') {
            // Setting says output full URLs - convert to full URL if not already
            if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
                $base_url = ee()->config->item('base_url') ?? '';
                // Fix malformed URLs missing a slash after the protocol
                if (preg_match('/^https?:\/[^\/]/', $base_url)) {
                    $base_url = preg_replace('/^(https?):\/([^\/])/', '$1://$2', $base_url);
                }
                $url = $base_url . ltrim($url, '/');
            }
        } else {
            // Setting says output relative URLs - strip base_url if present
            if (str_starts_with($url, 'http://') || str_starts_with($url, 'https://')) {
                $base_url = ee()->config->item('base_url') ?? '';
                if (!empty($base_url) && str_starts_with($url, $base_url)) {
                    // Remove base_url to make it relative
                    $url = '/' . ltrim(substr($url, strlen($base_url)), '/');
                }
            }
        }
        
        return $url;
    }
    
    /**
     * Update cache log for generated placeholder
     * 
    * @param string $placeholder_path Placeholder file path
     * @param string $filter_name Filter name
     * @return void
     */
    private function _update_placeholder_cache_log(string $placeholder_path, string $filter_name): void
    {
        try {
            $cache_management_service = $this->cache_service;
            $normalized_cache_dir = $this->context->get_metadata_value('cache_directory_normalized', '.');
            
            // Use all metadata for cache log (all template variables are stored as individual metadata keys)
            $vars_for_cache = $this->context->get_metadata();
            
            $cache_management_service->update_cache_log(
                image_path: $placeholder_path,
                processing_time: 0.01, // On-demand generation is very fast
                vars: $vars_for_cache,
                cache_dir: $normalized_cache_dir,
                source_path: $this->context->get_metadata_value('source_path', ''),
                force_update: true,
                using_cache_copy: false
            );
            
        } catch (\Exception $e) {
            $this->utilities_service->debug_error('placeholder_cache_log_update_error', $e);
        }
    }
}
