<?php

/**
 * JCOGS Image Pro - After File Update Extension
 * ==============================================
 * Handles cache cleanup when files are saved/updated or deleted from the file manager
 * 
 * @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\Extensions;

use ExpressionEngine\Service\Addon\Controllers\Extension\AbstractRoute;
use JCOGSDesign\JCOGSImagePro\Service\ServiceCache;

/**
 * After File Update Extension
 * 
 * Consolidated extension handling cleanup of cached images when source files 
 * are updated or deleted. Follows Legacy pattern where a single method handles
 * both after_file_save and after_file_delete hooks.
 * 
 * Migrated from Legacy ext.jcogs_img.php->after_file_update() with
 * enhanced Pro architecture using ServiceCache pattern.
 * 
 * Key Features:
 * - Handles both file saves/updates and deletions
 * - Automatic cache cleanup when auto-manage is enabled
 * - Cache impact notifications when auto-manage is disabled
 * - Integration with Pro cache management service
 * - Support for multiple filesystem adapters
 * - Single method pattern reducing code duplication
 */
class AfterFileUpdate extends AbstractRoute
{
    protected $addon_name = 'jcogs_img_pro';
    protected $version = '2.0.0';
    
    /**
     * @var object Settings service instance
     */
    private $settings_service;
    
    /**
     * @var object Cache management service instance
     */
    private $cache_service;
    
    /**
     * @var object Utilities service instance
     */
    private $utilities_service;
    
    /**
     * Constructor - Initialize services using ServiceCache
     */
    public function __construct()
    {
        // Use ServiceCache for optimal performance and consistency
        $this->settings_service = ServiceCache::settings();
        $this->cache_service = ServiceCache::cache();
        $this->utilities_service = ServiceCache::utilities();
    }
    
    /**
     * Process file update (save/update or delete) - check for affected cache entries
     * 
     * Consolidated method that handles both after_file_save and after_file_delete hooks.
     * Migrated from Legacy ext.jcogs_img.php->after_file_update() with
     * enhanced Pro architecture.
     * 
     * This follows the Legacy pattern where a single method is registered for
     * multiple hooks via database configuration, reducing code duplication.
     * 
     * @param mixed $file File object or ID being saved/updated/deleted
     * @param array $values File data array with update information
     * @return mixed Returns the file object unchanged to allow processing to continue
     */
    public function after_file_update($file, $values)
    {
        // Input validation - allow either payload shape (values array and/or file model).
        if (empty($file) && empty($values)) {
            return $file;
        }

        if (!is_array($values)) {
            $values = [];
        }
        
        try {
            // Get file information for cache lookup
            $file_info = $this->_extract_file_information($file, $values);
            if (!$file_info) {
                return $file; // Unable to extract file info, allow operation to proceed
            }
            
            // Search for affected cache entries
            $affected_images = $this->_find_affected_cache_entries($file_info);
            
            if (empty($affected_images)) {
                return $file; // No affected images found
            }
            
            // Check auto-manage setting
            $auto_manage_enabled = $this->_is_auto_manage_enabled();
            
            if ($auto_manage_enabled) {
                // Auto-manage enabled - delete affected cache entries
                $this->_cleanup_affected_cache_entries($affected_images, $file_info);
            } else {
                // Auto-manage disabled - show notification
                $this->_show_cache_impact_notification(count($affected_images));
            }
            
        } catch (\Throwable $e) {
            // Log error but don't prevent file operation
            $this->utilities_service->debug_log("Error in AfterFileUpdate: " . $e->getMessage());
        }
        
        return $file; // Always return file object unchanged to allow operation to proceed
    }
    
    /**
     * Extract file information from file operation data
     * 
     * @param mixed $file File model or file payload object
     * @param array $values File data from save/delete event
     * @return array|null File information array or null if invalid
     */
    private function _extract_file_information($file, array $values): ?array
    {
        // Resolve filename from values payload first, then file model fallback.
        $file_name = $this->_resolve_file_name($file, $values);
        if ($file_name === '') {
            return null;
        }
        
        $filename_array = pathinfo($file_name);
        if (!$filename_array || empty($filename_array['filename'])) {
            return null;
        }
        
        // Only process image files - skip non-image files like documents, videos, etc.
        // This prevents unnecessary cache lookups for files that IMG Pro never processes
        // Use server capabilities to determine supported image extensions dynamically
        $extension = strtolower($filename_array['extension'] ?? '');
        $image_extensions = $this->_get_supported_image_extensions();
        if (!in_array($extension, $image_extensions)) {
            return null; // Not an image file, skip processing
        }
        
        // Resolve upload location id from values payload first, then file model fallback.
        $upload_location_id = $this->_resolve_upload_location_id($file, $values);
        if ($upload_location_id <= 0) {
            return null;
        }
        
        // Resolve upload destination directly from EE model so we get server_path/url reliably.
        $upload_destination = ee('Model')->get('UploadDestination')
            ->filter('id', $upload_location_id)
            ->first();
        if (!$upload_destination) {
            return null;
        }
        
        $server_path = trim((string) ($upload_destination->server_path ?? ''));
        if ($server_path === '') {
            return null;
        }
        
        // Build file information array following Legacy structure
        $filename_array['source_path'] = rtrim(str_replace('\\', '/', $server_path), '/');
        $source_candidates = $this->_build_source_candidates($upload_destination, $file_name);
        
        return [
            'filename' => $filename_array['filename'],
            'extension' => $filename_array['extension'] ?? '',
            'source_path' => $filename_array['source_path'],
            'upload_location_id' => $upload_location_id,
            'source_candidates' => $source_candidates,
        ];
    }

    /**
     * Resolve file name from hook values or file model.
     *
     * @param mixed $file
     * @param array $values
     * @return string
     */
    private function _resolve_file_name($file, array $values): string
    {
        $file_name = $values['file_name'] ?? '';
        if (is_string($file_name) && trim($file_name) !== '') {
            return trim($file_name);
        }

        if (is_object($file) && isset($file->file_name) && is_string($file->file_name) && trim($file->file_name) !== '') {
            return trim($file->file_name);
        }

        return '';
    }

    /**
     * Resolve upload location id from hook values or file model.
     *
     * @param mixed $file
     * @param array $values
     * @return int
     */
    private function _resolve_upload_location_id($file, array $values): int
    {
        if (isset($values['upload_location_id']) && (int) $values['upload_location_id'] > 0) {
            return (int) $values['upload_location_id'];
        }

        if (is_object($file) && isset($file->upload_location_id) && (int) $file->upload_location_id > 0) {
            return (int) $file->upload_location_id;
        }

        return 0;
    }
    
    /**
     * Find cache entries affected by file changes
     * 
     * @param array $file_info File information for lookup
     * @return array Array of affected cache entries
     */
    private function _find_affected_cache_entries(array $file_info): array
    {
        $candidate_paths = $file_info['source_candidates'] ?? [];

        // Backward-compatible fallback if candidate generation failed for any reason.
        if (empty($candidate_paths)) {
            $candidate_paths = [
                rtrim($file_info['source_path'], '/') . '/' . $file_info['filename'] . '.' . $file_info['extension'],
            ];
        }

        $matches = [];
        $seen = [];

        foreach ($candidate_paths as $candidate_path) {
            if (!is_string($candidate_path) || trim($candidate_path) === '') {
                continue;
            }

            $entries = $this->cache_service->find_entries_by_source_file($candidate_path);
            if (empty($entries)) {
                continue;
            }

            foreach ($entries as $entry) {
                $dedupe_key = $entry['id'] ?? (($entry['path'] ?? '') . '|' . ($entry['image_name'] ?? ''));
                if ($dedupe_key === '' || isset($seen[$dedupe_key])) {
                    continue;
                }

                $seen[$dedupe_key] = true;
                $matches[] = $entry;
            }
        }

        return $matches;
    }

    /**
     * Build robust source candidates for cache source matching.
     *
     * @param object $upload_destination UploadDestination model
     * @param string $file_name File name from EE hook payload
     * @return array
     */
    private function _build_source_candidates(object $upload_destination, string $file_name): array
    {
        $file_name = ltrim(str_replace('\\', '/', trim($file_name)), '/');
        if ($file_name === '') {
            return [];
        }

        $candidates = [];

        $server_path = trim((string) ($upload_destination->server_path ?? ''));
        if ($server_path !== '') {
            $absolute_path = rtrim(str_replace('\\', '/', $server_path), '/') . '/' . $file_name;
            $candidates[] = $absolute_path;
            $candidates[] = ltrim($absolute_path, '/');

            $base_path = trim((string) (ee()->config->item('base_path') ?? ''));
            if ($base_path !== '') {
                $normalized_base_path = rtrim(str_replace('\\', '/', $base_path), '/');
                if (str_starts_with(strtolower($absolute_path), strtolower($normalized_base_path . '/'))) {
                    $candidates[] = ltrim(substr($absolute_path, strlen($normalized_base_path)), '/');
                }
            }
        }

        $url_path = trim((string) ($upload_destination->url ?? ''));
        if ($url_path !== '') {
            $url_candidate = rtrim(str_replace('\\', '/', $url_path), '/') . '/' . $file_name;
            $candidates[] = $url_candidate;
            $candidates[] = ltrim($url_candidate, '/');
        }

        return array_values(array_unique(array_filter($candidates, static function ($path) {
            return is_string($path) && trim($path) !== '';
        })));
    }
    
    /**
     * Check if auto-manage cache setting is enabled
     * 
     * @return bool True if auto-manage is enabled
     */
    private function _is_auto_manage_enabled(): bool
    {
        // Settings service exposes a full settings array; resolve known key aliases.
        $settings = [];
        if (is_object($this->settings_service) && method_exists($this->settings_service, 'all')) {
            $settings = (array) $this->settings_service->all();
        }

        $auto_manage_cache_setting = $settings['img_cp_cache_auto_manage']
            ?? $settings['auto_manage_cache']
            ?? 'n';

        return strtolower(trim((string) $auto_manage_cache_setting)) === 'y';
    }
    
    /**
     * Clean up affected cache entries when auto-manage is enabled
     * 
     * @param array $affected_images Array of affected cache entries
     * @param array $file_info File information for logging
     * @return void
     */
    private function _cleanup_affected_cache_entries(array $affected_images, array $file_info): void
    {
        $deleted_count = 0;
        
        foreach ($affected_images as $cache_entry) {
            try {
                if (!is_array($cache_entry) || empty($cache_entry['path'])) {
                    continue;
                }

                $cache_path = trim((string) $cache_entry['path'], '/');
                $connection_name = $cache_entry['adapter_name'] ?? null;

                if ($this->cache_service->remove_cache_entry_by_path($cache_path, $connection_name)) {
                    $deleted_count++;
                }
            } catch (\Exception $e) {
                $this->utilities_service->debug_log("Failed to delete cache entry: " . $e->getMessage());
            }
        }
        
        if ($deleted_count > 0) {
            // Show success message
            $this->_show_cleanup_success_notification($deleted_count, $file_info['filename']);
        }
    }
    
    /**
     * Show notification about cache impact when auto-manage is disabled
     * 
     * @param int $affected_count Number of affected cache entries
     * @return void
     */
    private function _show_cache_impact_notification(int $affected_count): void
    {
        if (REQ === 'CP') {
            ee('CP/Alert')->makeInline('jcogs_img_pro_cache_impact')
                ->asWarning()
                ->withTitle('JCOGS Image Pro Cache Impact')
                ->addToBody("File changes may affect {$affected_count} cached image(s). Consider enabling auto-manage cache or manually clearing affected entries.")
                ->now();
        }
    }
    
    /**
     * Show success notification for cache cleanup
     * 
     * @param int $deleted_count Number of cache entries deleted
     * @param string $filename Original filename
     * @return void
     */
    private function _show_cleanup_success_notification(int $deleted_count, string $filename): void
    {
        if (REQ === 'CP') {
            ee('CP/Alert')->makeInline('jcogs_img_pro_cache_cleanup')
                ->asSuccess()
                ->withTitle('JCOGS Image Pro Cache Cleanup')
                ->addToBody("Automatically removed {$deleted_count} cached image(s) affected by changes to '{$filename}'.")
                ->now();
        }
    }
    
    /**
     * Get supported image extensions from server capabilities
     * 
     * Uses FormatCapabilityDetection service to dynamically determine which
     * image formats the server can process. This ensures we only monitor
     * file operations for formats that IMG Pro can actually handle.
     * 
     * Static cache prevents repeated server capability checks within same request.
     * 
     * @return array Array of supported image file extensions
     */
    private function _get_supported_image_extensions(): array
    {
        static $supported_extensions = null;
        
        if ($supported_extensions !== null) {
            return $supported_extensions;
        }
        
        try {
            // Use FormatCapabilityDetection service to get server capabilities
            $format_detection = new \JCOGSDesign\JCOGSImagePro\Service\FormatCapabilityDetection();
            $server_capabilities = $format_detection->getServerCapabilities();
            
            // Extract format names (keys) from capabilities array
            // These are the formats the server can actually process
            $supported_extensions = array_keys($server_capabilities);
            
            // Always include SVG (passthrough support)
            if (!in_array('svg', $supported_extensions)) {
                $supported_extensions[] = 'svg';
            }
            
            // Cache the result
            return $supported_extensions;
            
        } catch (\Exception $e) {
            // Fallback to conservative list if capability detection fails
            $this->utilities_service->debug_log('Failed to detect server capabilities, using fallback list: ' . $e->getMessage());
            $supported_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'tif', 'svg'];
            return $supported_extensions;
        }
    }
}

