<?php

/**
 * JCOGS Image Pro - Abstract Route Base Class
 * ============================================
 * Base class for all Image Pro control panel routes with shared service initialization
 * 
 * @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\ControlPanel\Routes;

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

/**
 * Abstract Route Class with Shared Services
 * 
 * Extends EE's AbstractRoute and provides automatic initialization of common services
 * using the ServiceCache pattern for optimal performance.
 * 
 * All Image Pro control panel routes should extend this class to inherit:
 * - Shared service instances (settings, utilities, filesystem, etc.)
 * - Common functionality for Image Pro routes
 * - Automatic service optimization
 */
abstract class ImageAbstractRoute extends AbstractRoute
{
    /**
     * Shared services available to all Image Pro routes (using ServiceCache for optimal performance)
     */
    protected $settings_service;
    protected $utilities_service;
    protected $filesystem_service;
    protected $validation_service;
    protected $performance_service;
    protected $colour_service;
    protected $cache_service;
    protected $preset_service;
    protected $license_guard;
    protected $license_notice_renderer;
    protected $license_policy_decision = [];
    protected $last_settings_save_error = '';

    /**
    * Build a scheme-safe ACT endpoint URL for AJAX requests.
    *
    * Uses ExpressionEngine's site index to derive a root-relative front
    * controller path (for example `/index.php`), then appends ACT query
    * parameters. Returning a relative URL avoids host/scheme drift behind
    * proxies and eliminates mixed-content errors caused by absolute HTTP URLs.
     *
     * @param string $class Action class name
     * @param string $action_method Registered ACT handler method (exp_actions.method)
     * @param string|null $endpoint_method Optional endpoint method query parameter
     * @return string Fully-qualified ACT URL
     */
    protected function buildActionEndpointUrl(string $class, string $action_method, ?string $endpoint_method = null): string
    {
        $endpoint_method = $endpoint_method ?? $action_method;

        $action_id = ee()->db->select('action_id')
            ->from('actions')
            ->where('class', $class)
            ->where('method', $action_method)
            ->get()
            ->row('action_id');

        if (is_array($action_id)) {
            $action_id = $action_id['action_id'] ?? '';
        }

        $site_index = ee()->functions->fetch_site_index(0, 0);
        $site_index_path = parse_url($site_index, PHP_URL_PATH);

        if (!is_string($site_index_path) || $site_index_path === '') {
            $site_index_path = '/';
        }

        return $site_index_path . QUERY_MARKER . 'ACT=' . (string) $action_id . '&method=' . urlencode($endpoint_method);
    }

    /**
     * Determine if current request is HTTPS, including proxy headers.
     *
     * @return bool
     */
    protected function isSecureRequest(): bool
    {
        if (!empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off') {
            return true;
        }

        if (isset($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443) {
            return true;
        }

        if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
            $proto = strtolower(trim((string) $_SERVER['HTTP_X_FORWARDED_PROTO']));
            if (strpos($proto, 'https') !== false) {
                return true;
            }
        }

        if (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && strtolower((string) $_SERVER['HTTP_X_FORWARDED_SSL']) === 'on') {
            return true;
        }

        return false;
    }
    
    /**
     * Constructor - automatically initializes shared services
     * 
     * Called automatically when any route extending this class is instantiated.
     * Provides immediate access to all common services without repeated instantiation.
     */
    public function __construct()
    {
        parent::__construct();
        
        // Initialize shared services using ServiceCache for optimal performance
        $this->settings_service = ServiceCache::settings();
        $this->utilities_service = ServiceCache::utilities();
        $this->filesystem_service = ServiceCache::filesystem();
        $this->validation_service = ServiceCache::validation();
        $this->performance_service = ServiceCache::performance();
        $this->colour_service = ServiceCache::colour();
        $this->cache_service = ServiceCache::cache();
        $this->preset_service = ServiceCache::preset_service();

        $this->initializeLicenseGovernance();
    }

    /**
     * Initialize CP licensing governance services and preflight decision.
     *
     * This is intentionally CP-only and does not run in front-end paths.
     *
     * @return void
     */
    protected function initializeLicenseGovernance(): void
    {
        if (!(defined('REQ') && REQ === 'CP')) {
            return;
        }

        try {
            $this->license_guard = $this->resolveLicenseGuard();
            $this->license_notice_renderer = $this->resolveLicenseNoticeRenderer();

            $route_path = property_exists($this, 'route_path') ? (string) ($this->route_path ?? 'index') : 'index';
            $is_post = !empty($_POST);

            $this->license_policy_decision = $this->license_guard->preflightRoute($route_path, $is_post);
        } catch (\Throwable $e) {
            // Governance bootstrap should not block CP route rendering.
            $this->license_policy_decision = [];
            if (isset($this->utilities_service)) {
                $this->utilities_service->debug_message(
                    'License governance bootstrap failed: ' . $e->getMessage(),
                    'ImageAbstractRoute::initializeLicenseGovernance'
                );
            }
        }
    }

    /**
     * Resolve guard service with DI-first, manual fallback approach.
     */
    private function resolveLicenseGuard()
    {
        try {
            return ee('jcogs_img_pro:CpLicenseGuard');
        } catch (\Throwable $e) {
            $status = new \JCOGSDesign\JCOGSImagePro\Service\License\CpLicenseStatusService();
            $repo   = new \JCOGSDesign\JCOGSImagePro\Service\License\LicenseRuntimeRepository();
            $policy = new \JCOGSDesign\JCOGSImagePro\Service\License\CpLicensePolicyService($status, $repo);

            return new \JCOGSDesign\JCOGSImagePro\Service\License\CpLicenseGuard($policy);
        }
    }

    /**
     * Resolve notice renderer service with DI-first, manual fallback approach.
     */
    private function resolveLicenseNoticeRenderer()
    {
        try {
            return ee('jcogs_img_pro:CpLicenseNoticeRenderer');
        } catch (\Throwable $e) {
            return new \JCOGSDesign\JCOGSImagePro\Service\License\CpLicenseNoticeRenderer();
        }
    }

    /**
     * Render inline licensing notice (if policy indicates one is needed).
     *
     * @return void
     */
    protected function displayLicenseGovernanceNotice(): void
    {
        if (!(defined('REQ') && REQ === 'CP')) {
            return;
        }

        if (!$this->license_notice_renderer || empty($this->license_policy_decision)) {
            $this->initializeLicenseGovernance();
        }

        if (!$this->license_notice_renderer || empty($this->license_policy_decision)) {
            return;
        }

        try {
            $notice_data = $this->license_notice_renderer->renderInlineNotice($this->license_policy_decision);
            if ($notice_data === null) {
                return;
            }

            $alert = ee('CP/Alert')->makeInline('jcogs-img-pro-license-governance')
                ->withTitle($notice_data['title'])
                ->addToBody($notice_data['body']);

            switch ($notice_data['severity'] ?? 'info') {
                case 'error':
                    $alert->asIssue();
                    break;
                case 'warning':
                    $alert->asWarning();
                    break;
                default:
                    $alert->asAttention();
                    break;
            }

            $alert->now();
        } catch (\Throwable $e) {
            if (isset($this->utilities_service)) {
                $this->utilities_service->debug_message(
                    'License notice render failed: ' . $e->getMessage(),
                    'ImageAbstractRoute::displayLicenseGovernanceNotice'
                );
            }
        }
    }

    /**
     * Accessor for policy decision data from CP preflight.
     *
     * @return array
     */
    protected function getLicensePolicyDecision(): array
    {
        return $this->license_policy_decision;
    }

    /**
     * Enforce mutation policy for CP write actions.
     *
     * @param string $actionLabel Human-readable action label for diagnostics
     * @param string $redirectRoute Route suffix under addons/settings/jcogs_img_pro
     * @return bool
     */
    protected function ensureMutationAllowed(string $actionLabel, string $redirectRoute): bool
    {
        if (!(defined('REQ') && REQ === 'CP')) {
            return true;
        }

        if (!$this->license_guard) {
            $this->initializeLicenseGovernance();
        }

        if (!$this->license_guard) {
            return true;
        }

        try {
            $this->license_guard->assertCanMutate($actionLabel);
            return true;
        } catch (\RuntimeException $e) {
            $this->loadLanguage('jcogs_img_pro_license');

            ee('CP/Alert')->makeInline('jcogs-img-pro-license-mutation-blocked')
                ->asIssue()
                ->withTitle(lang('jcogs_img_pro_license_required_title') ?: 'License Required')
                ->addToBody(lang('jcogs_img_pro_license_required_body') ?: 'Control Panel changes are currently restricted until a valid license is configured.')
                ->defer();

            $this->redirectToRoute($redirectRoute);
            return false;
        } catch (\Throwable $e) {
            // Fail open for unexpected guard/runtime failures to avoid CP lockout.
            if (isset($this->utilities_service)) {
                $this->utilities_service->debug_message(
                    'License mutation guard failure: ' . $e->getMessage(),
                    'ImageAbstractRoute::ensureMutationAllowed'
                );
            }

            return true;
        }
    }
    
    /**
     * Common functionality for Image Pro routes
     */
    
    /**
     * Build sidebar using common method (available to all extending routes)
     * 
     * @param array $current_settings Current settings array
     * @return void
     */
    protected function build_sidebar(array $current_settings): void
    {
        $this->displayLicenseGovernanceNotice();
        $this->utilities_service->build_sidebar($current_settings);
    }
    
    /**
     * Display error notifications with enhanced UI
     * Shows a single summary message that can be expanded to show details
     * 
     * @return void
     */
    protected function displayErrorNotifications(): void
    {
        try {
            $error_service = new \JCOGSDesign\JCOGSImagePro\Service\ErrorNotificationService();
            $unread_count = $error_service->getUnreadNotificationCount();
            
            if ($unread_count === 0) {
                return;
            }
            
            // Generate summary message
            $summary_title = sprintf(lang('jcogs_img_pro_error_summary_title') ?: 'Image Pro Errors: %d new errors detected', $unread_count);
            $summary_body = $this->buildErrorSummaryBody($unread_count, $error_service);
            
            // Display single summary notification
            ee('CP/Alert')->makeInline('img-pro-error-summary')
                ->asWarning()
                ->withTitle($summary_title)
                ->addToBody($summary_body)
                ->now();
                
        } catch (\Throwable $e) {
            // Don't let error notification display break the Control Panel
            // Log the issue for debugging
            if (isset($this->utilities_service)) {
                $this->utilities_service->debug_message(
                    'Failed to display error notifications: ' . $e->getMessage(),
                    'ImageAbstractRoute::displayErrorNotifications'
                );
            }
        }
    }
    
    /**
     * Build error summary body with expandable details
     * 
     * @param int $unread_count Number of unread notifications
     * @param object $error_service Error notification service instance
     * @return string HTML content for the summary body
     */
    private function buildErrorSummaryBody(int $unread_count, $error_service): string
    {
        // Load JavaScript assets for error details functionality
        $this->loadErrorDetailsAssets();
        
        $view_details_text = lang('jcogs_img_pro_view_error_details') ?: 'view details';
        $explanation_text = lang('jcogs_img_pro_error_explanation') ?: 'During recent template parsing, Image Pro noticed that one or more of its tags failed to generate output due to an error.';
        
        $html = '<div class="jcogs-error-summary" data-error-count="' . $unread_count . '">';
        $html .= '<div class="jcogs-error-summary-header">';
        $html .= '<div class="jcogs-error-summary-text">';
        $html .= '<p class="jcogs-error-summary-explanation">' . $explanation_text . '</p>';
        $html .= '</div>';
        $html .= '<div class="jcogs-error-summary-button">';
        $html .= '<a href="#" class="jcogs-expand-errors btn btn-sm btn-default" data-toggle="collapse" data-target="#jcogs-error-details">' . $view_details_text . '</a>';
        $html .= '</div>';
        $html .= '</div>';
        
        // Collapsible details section - initially empty, no loading placeholder
        $html .= '<div id="jcogs-error-details" class="collapse"></div>';
        
        $html .= '</div>';
        
        return $html;
    }
    
    /**
     * Load JavaScript assets for error details functionality
     * 
     * @return void
     */
    private function loadErrorDetailsAssets(): void
    {
        $asset_version = defined('JCOGS_IMG_PRO_VERSION') ? JCOGS_IMG_PRO_VERSION : 'dev';
        
        // Load CSS for error details
        ee()->cp->add_to_head('<link rel="stylesheet" type="text/css" href="' . URL_THIRD_THEMES . 'jcogs_img_pro/css/error-details.css" />');
        
        // Use CP route endpoints to avoid front-end ACT throttling path.
        $ajax_url = (string) ee('CP/URL')->make('addons/settings/jcogs_img_pro/error_details_data');
        $ajax_url_all = (string) ee('CP/URL')->make('addons/settings/jcogs_img_pro/error_details_all_data');
        
        // Add JavaScript variables to page footer (before external script loads)
        ee()->cp->add_to_foot('<script>window.jcogsImageProErrorUrls = {errorDetails: ' . json_encode($ajax_url) . ', errorDetailsAll: ' . json_encode($ajax_url_all) . '};</script>');
        
        // Load external JavaScript file for error details functionality
        ee()->cp->add_to_foot('<script defer src="' . URL_THIRD_THEMES . 'jcogs_img_pro/javascript/error-details.js?v=' . rawurlencode((string)$asset_version) . '"></script>');
    }
    
    /**
     * Format notification message for Control Panel display
     * 
     * @param array $notification Notification data
     * @return string Formatted message
     */
    protected function formatNotificationMessage(array $notification): string
    {
        $message = '<div class="error-notification">';
        $message .= '<p style="color: #666; font-size: 0.9em;">' . htmlspecialchars($notification['short_message'], ENT_QUOTES, 'UTF-8') . '</p>';
        $message .= '<p><strong>' . htmlspecialchars($notification['error_message'], ENT_QUOTES, 'UTF-8') . '</strong></p>';
        
        if (!empty($notification['template_name'])) {
            $message .= '<p style="color: #666; font-size: 0.9em;"><em>' . lang('jcogs_img_pro_notification_template_label') . ' ' . htmlspecialchars($notification['template_name'], ENT_QUOTES, 'UTF-8') . '</em></p>';
        }
        
        $message .= '<p style="color: #999; font-size: 0.85em;">' . lang('jcogs_img_pro_notification_occurred_label') . ' ' . htmlspecialchars($notification['created_at_formatted'], ENT_QUOTES, 'UTF-8') . '</p>';
        
        $message .= '</div>';
        
        return $message;
    }
    
    /**
     * Get current settings with optional defaults and session cache fallback
     * 
     * @param array $defaults Optional default values to merge
     * @param bool $use_session_cache Whether to check session cache for temporary settings
     * @return array Current settings with optional defaults
     */
    protected function getCurrentSettings(array $defaults = [], bool $use_session_cache = false): array
    {
        // Get settings from the service with fallback mechanism
        if ($this->settings_service && method_exists($this->settings_service, 'all')) {
            $settings = $this->settings_service->all();
        } elseif ($this->settings_service && method_exists($this->settings_service, 'get_all')) {
            $settings = $this->settings_service->get_all();
        } else {
            // Fallback to basic cache settings if no service method available
            $settings = [
                'img_cp_default_cache_duration' => ee()->config->item('jcogs_img_pro_default_cache_duration') ?? '2678400',
                'img_cp_cache_auto_manage' => ee()->config->item('jcogs_img_pro_cache_auto_manage') ?? 'y',
            ];
        }
        
        // Check session cache for temporary settings (if requested)
        if ($use_session_cache) {
            $session_settings = ee()->session->cache(__CLASS__, 'jcogs_img_pro_settings', []);
            if (!empty($session_settings)) {
                $settings = array_merge($settings, $session_settings);
            }
        }
        
        // Merge with provided defaults (defaults are applied first, then overridden by actual settings)
        if (!empty($defaults)) {
            return array_merge($defaults, $settings);
        }
        
        return $settings;
    }
    
    /**
     * Load Image Pro language file (available to all extending routes)
     * 
     * @param string $language_key Optional specific language key
     * @return void
     */
    protected function loadLanguage(string $language_key = 'jcogs_img_pro'): void
    {
        ee()->lang->load($language_key, ee()->session->get_language(), false, true, PATH_THIRD . 'jcogs_img_pro/');
    }
    
    /**
     * Redirect to route (available to all extending routes)
     * 
     * @param string $route_name Route name to redirect to
     * @return void
     */
    protected function redirectToRoute(string $route_name): void
    {
        ee()->functions->redirect(ee('CP/URL')->make('addons/settings/jcogs_img_pro/' . $route_name));
    }
    
    /**
     * Save settings with validation (available to all extending routes)
     * 
     * @param array $posted_data Posted form data
     * @return bool Success status
     */
    protected function saveSettings(array $posted_data): bool
    {
        $this->last_settings_save_error = '';

        try {
            // Persist only real settings keys and scalar values.
            // This prevents CSRF/submit/form helper keys from being inserted
            // into settings storage and avoids environment-specific DB errors.
            $known_settings = $this->settings_service->get_all();
            $sanitized_settings = [];

            foreach ($posted_data as $key => $value) {
                if (!is_string($key) || $key === '') {
                    continue;
                }

                if (is_array($value) || is_object($value)) {
                    continue;
                }

                $is_known_setting = array_key_exists($key, $known_settings);
                $is_setting_prefix = str_starts_with($key, 'img_cp_') || str_starts_with($key, 'jcogs_');

                if (!$is_known_setting && !$is_setting_prefix) {
                    continue;
                }

                if (is_string($value)) {
                    $sanitized_settings[$key] = trim($value);
                } elseif ($value === null) {
                    $sanitized_settings[$key] = '';
                } else {
                    $sanitized_settings[$key] = (string) $value;
                }
            }

            $this->settings_service->save_settings($sanitized_settings);
            return true;
        } catch (\Throwable $e) {
            $this->last_settings_save_error = $e->getMessage();
            // Log error using shared utilities service
            $this->utilities_service->debug_log('Settings save error: ' . $e->getMessage());
            return false;
        }
    }

    /**
     * Get details of the last settings-save error.
     */
    protected function getLastSettingsSaveError(): string
    {
        return (string) $this->last_settings_save_error;
    }
    
    /**
     * Set success flash message (available to all extending routes)
     * 
     * @param string $message_key Language key for success message
     * @return void
     */
    protected function set_success_message(string $message_key): void
    {
        ee()->session->set_flashdata('message_success', lang($message_key));
    }
    
    /**
     * Validate and load preset from URL
     * 
     * Common pattern used by preset routes to extract preset ID from URL,
     * validate it exists, and load the preset data. Shows appropriate error
     * alerts and redirects if validation fails.
     * 
     * @param string $error_prefix Prefix for error alert IDs (e.g., 'edit_error')
     * @param string $redirect_route Route to redirect to on error (default: 'presets')
     * @return array|null Preset data or null if validation failed
     */
    protected function validateAndLoadPresetFromUrl(string $error_prefix = 'preset_error', string $redirect_route = 'presets'): ?array
    {
        // Get preset ID from URL segments
        $preset_id = $this->utilities_service->getPresetIdFromUrl();
        
        if (empty($preset_id)) {
            ee('CP/Alert')->makeInline($error_prefix . '_no_id')
                ->asIssue()
                ->withTitle('Invalid Request')
                ->addToBody('No preset ID specified.')
                ->defer();
                
            ee()->functions->redirect(ee('CP/URL')->make('addons/settings/jcogs_img_pro/' . $redirect_route));
            return null;
        }
        
        // Load the preset using centralized PresetService
        $preset = $this->preset_service->getPresetById($preset_id);
        if (!$preset) {
            ee('CP/Alert')->makeInline($error_prefix . '_not_found')
                ->asIssue()
                ->withTitle('Preset Not Found')
                ->addToBody('The specified preset could not be found.')
                ->defer();
                
            ee()->functions->redirect(ee('CP/URL')->make('addons/settings/jcogs_img_pro/' . $redirect_route));
            return null;
        }
        
        return $preset;
    }
}