<?php

/**
 * JCOGS Image Pro - Error Notification Service
 * ============================================
 * Captures and manages error notifications for Control Panel display
 * 
 * @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 - Issue #89 Error Notification System
 */

namespace JCOGSDesign\JCOGSImagePro\Service;

use Exception;
use Throwable;
use JCOGSDesign\JCOGSImagePro\Service\ServiceCache;
use JCOGSDesign\JCOGSImagePro\Service\Utilities;

class ErrorNotificationService
{
    /**
     * @var string Database table name for error notifications
     */
    private string $table_name;
    
    /**
     * @var int Site ID for multi-site compatibility
     */
    private int $site_id;
    
    /**
     * @var Utilities Utilities service for logging
     */
    private Utilities $utilities;
    
    /**
     * @var int Maximum number of error notifications to retain
     */
    private const MAX_NOTIFICATIONS = 100;
    
    /**
     * @var int Number of notifications to remove when trimming (removes oldest)
     */
    private const TRIM_COUNT = 40;
    
    /**
     * @var int Number of days after which notifications are considered old
     */
    private const MAX_AGE_DAYS = 30;
    
    /**
     * Constructor
     */
    public function __construct()
    {
        $this->table_name = 'jcogs_img_pro_error_notifications';
        $this->site_id = ee()->config->item('site_id');
        $this->utilities = ServiceCache::utilities();
    }
    
    /**
     * Perform automatic cleanup of error notifications
     * 
     * This method is called automatically when logging errors to prevent
     * unbounded table growth. It implements two cleanup strategies:
     * 
     * 1. Count-based: If > MAX_NOTIFICATIONS, remove TRIM_COUNT oldest records
     * 2. Age-based: Remove records older than MAX_AGE_DAYS
     * 
     * Count-based trimming happens first to handle the immediate issue of
     * too many records (e.g., from loops generating duplicate errors).
     * The TRIM_COUNT buffer (40 records) prevents constant cleanup at the limit.
     * 
     * @return array Cleanup statistics ['count_trimmed' => int, 'age_trimmed' => int]
     */
    private function performAutomaticCleanup(): array
    {
        $stats = ['count_trimmed' => 0, 'age_trimmed' => 0];
        
        try {
            // Check table existence
            if (!ee()->db->table_exists($this->table_name)) {
                return $stats;
            }
            
            // 1. Count-based cleanup - trim oldest records if over limit
            $current_count = ee()->db->where('site_id', $this->site_id)
                ->count_all_results($this->table_name);
            
            if ($current_count > self::MAX_NOTIFICATIONS) {
                // Get IDs of oldest records to remove
                $to_remove = $current_count - (self::MAX_NOTIFICATIONS - self::TRIM_COUNT);
                
                $old_ids = ee()->db->select('id')
                    ->from($this->table_name)
                    ->where('site_id', $this->site_id)
                    ->order_by('created_at', 'ASC')
                    ->limit($to_remove)
                    ->get()
                    ->result_array();
                
                if (!empty($old_ids)) {
                    $ids_to_delete = array_column($old_ids, 'id');
                    
                    ee()->db->where('site_id', $this->site_id)
                        ->where_in('id', $ids_to_delete)
                        ->delete($this->table_name);
                    
                    $stats['count_trimmed'] = count($ids_to_delete);
                    
                    $this->utilities->debug_log(
                        "Trimmed {$stats['count_trimmed']} old notifications (count-based cleanup, limit: " . self::MAX_NOTIFICATIONS . ")",
                        'ErrorNotificationService::performAutomaticCleanup'
                    );
                }
            }
            
            // 2. Age-based cleanup - remove records older than MAX_AGE_DAYS
            $cutoff_time = ee()->localize->now - (self::MAX_AGE_DAYS * 24 * 60 * 60);
            
            $age_query = ee()->db->where('site_id', $this->site_id)
                ->where('created_at <', $cutoff_time)
                ->delete($this->table_name);
            
            $stats['age_trimmed'] = ee()->db->affected_rows();
            
            if ($stats['age_trimmed'] > 0) {
                $this->utilities->debug_log(
                    "Removed {$stats['age_trimmed']} notifications older than " . self::MAX_AGE_DAYS . " days",
                    'ErrorNotificationService::performAutomaticCleanup'
                );
            }
            
        } catch (Exception $e) {
            $this->utilities->debug_log(
                "Error during automatic cleanup: " . $e->getMessage(),
                'ErrorNotificationService::performAutomaticCleanup'
            );
        }
        
        return $stats;
    }
    
    /**
     * Get count of unread notifications
     * 
     * @return int Number of unread notifications
     */
    public function getUnreadCount(): int
    {
        try {
            return (int) ee()->db->where('site_id', $this->site_id)
                ->where('is_read', 0)
                ->count_all_results($this->table_name);
                
        } catch (Exception $e) {
            return 0;
        }
    }
    
    /**
     * Get count of unread error notifications
     * 
     * @return int Count of unread notifications
     */
    public function getUnreadNotificationCount(): int
    {
        try {
            // Check table existence first
            if (!ee()->db->table_exists($this->table_name)) {
                return 0;
            }
            
            $query = ee()->db->select('COUNT(*) as count')
                ->from($this->table_name)
                ->where('site_id', $this->site_id)
                ->where('is_read', 0)
                ->get();
            
            $result = $query->row_array();
            return (int)($result['count'] ?? 0);
            
        } catch (Exception $e) {
            $this->utilities->debug_log(
                "Failed to get unread notification count: " . $e->getMessage(),
                'ErrorNotificationService::getUnreadNotificationCount'
            );
            return 0;
        }
    }
    
    /**
     * Get unread error notifications for Control Panel display
     * 
     * @param int $limit Maximum number of notifications to return
     * @return array Array of error notification data
     */
    public function getUnreadNotifications(int $limit = 10): array
    {
        try {
            // Check table existence
            if (!ee()->db->table_exists($this->table_name)) {
                $this->utilities->debug_message(
                    "Error notifications table '{$this->table_name}' does not exist!",
                    'ErrorNotificationService::getUnreadNotifications'
                );
                return [];
            }
            
            $query = ee()->db->select('*')
                ->from($this->table_name)
                ->where('site_id', $this->site_id)
                ->where('is_read', 0)
                ->order_by('created_at', 'DESC')
                ->limit($limit)
                ->get();
            
            $notifications = [];
            foreach ($query->result_array() as $row) {
                $notifications[] = $this->_formatNotificationForDisplay($row);
            }
            
            return $notifications;
            
        } catch (Exception $e) {
            $this->utilities->debug_log(
                "Failed to retrieve error notifications: " . $e->getMessage(),
                'ErrorNotificationService::getUnreadNotifications'
            );
            return [];
        }
    }
    
    /**
     * Get recent error notifications (both read and unread) for "Show All" functionality
     * 
     * @param int $limit Maximum number of notifications to return
     * @return array Array of error notification data
     */
    public function getRecentNotifications(int $limit = 50): array
    {
        try {
            // Check table existence
            if (!ee()->db->table_exists($this->table_name)) {
                $this->utilities->debug_message(
                    "Error notifications table '{$this->table_name}' does not exist!",
                    'ErrorNotificationService::getRecentNotifications'
                );
                return [];
            }
            
            $query = ee()->db->select('*')
                ->from($this->table_name)
                ->where('site_id', $this->site_id)
                ->order_by('created_at', 'DESC')
                ->limit($limit)
                ->get();
                        
            $notifications = [];
            foreach ($query->result_array() as $row) {
                $notifications[] = $this->_formatNotificationForDisplay($row);
            }
            
            return $notifications;
            
        } catch (Exception $e) {
            $this->utilities->debug_log(
                "Failed to retrieve recent notifications: " . $e->getMessage(),
                'ErrorNotificationService::getRecentNotifications'
            );
            return [];
        }
    }
    
    /**
     * Log a processing error for later Control Panel notification
     * 
     * @param Throwable $exception The exception that occurred
     * @param array $context Processing context (tag parameters, template info, etc.)
     * @param string $operation_type Type of operation (pipeline, cache, etc.)
     * @return bool Success status
     */
    public function logProcessingError(Throwable $exception, array $context = [], string $operation_type = 'unknown'): bool
    {
        try {
            // Issue #121: Ignore EE request throttling messages.
            // These are expected rate-limit responses, not Image Pro processing failures.
            if ($this->isThrottlingError($exception, $operation_type)) {
                return false;
            }

            // Prepare error data
            $error_data = [
                'site_id' => $this->site_id,
                'operation_type' => $operation_type,
                'error_message' => $exception->getMessage(),
                'error_code' => $exception->getCode(),
                'error_file' => $exception->getFile(),
                'error_line' => $exception->getLine(),
                'stack_trace' => $exception->getTraceAsString(),
                'context_data' => json_encode($this->_sanitiseContext($context)),
                'template_name' => $this->_extractTemplateName($context),
                'tag_parameters' => json_encode($this->_extractTagParameters($context)),
                'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
                'ip_address' => ee()->input->ip_address(),
                'created_at' => ee()->localize->now,
                'is_read' => 0
            ];
            
            // Check if table exists before attempting insert
            if (!ee()->db->table_exists($this->table_name)) {
                $this->utilities->debug_message(
                    "Error notifications table '{$this->table_name}' does not exist! Please run database migrations.",
                    'ErrorNotificationService::logProcessingError'
                );
                return false;
            }
            
            // Insert into database
            $result = ee()->db->insert($this->table_name, $error_data);
            
            // Debug the insert result
            if ($result === false) {
                $this->utilities->debug_log(
                    "Failed to insert error notification into database. DB Error: " . ee()->db->error()['message'],
                    'ErrorNotificationService::logProcessingError'
                );
            } else {
                $this->utilities->debug_log(
                    "Successfully inserted error notification ID: " . ee()->db->insert_id(),
                    'ErrorNotificationService::logProcessingError'
                );
                
                // Perform automatic cleanup to prevent unbounded table growth
                // This runs after successful insert to maintain table size limits
                $cleanup_stats = $this->performAutomaticCleanup();
                
                // Log cleanup activity if any records were removed
                if ($cleanup_stats['count_trimmed'] > 0 || $cleanup_stats['age_trimmed'] > 0) {
                    $this->utilities->debug_log(
                        sprintf(
                            "Automatic cleanup: removed %d by count limit, %d by age limit",
                            $cleanup_stats['count_trimmed'],
                            $cleanup_stats['age_trimmed']
                        ),
                        'ErrorNotificationService::logProcessingError'
                    );
                }
            }
            
            // Also log to standard debug for immediate debugging
            $this->utilities->debug_message(
                "Processing error logged for CP notification: " . $exception->getMessage(),
                'ErrorNotificationService::logProcessingError'
            );
            
            return $result !== false;
            
        } catch (Exception $e) {
            // Don't let error logging break the main process
            $this->utilities->debug_message(
                "Failed to log processing error: " . $e->getMessage(),
                'ErrorNotificationService::logProcessingError'
            );
            return false;
        }
    }

    /**
     * Detect whether an exception represents EE throttling/rate-limiting.
     *
     * Throttling responses can be localized by EE, so we match common
     * language-independent and known localized fragments.
     *
     * @param Throwable $exception
    * @param string $operation_type Reserved for future pattern tuning.
     * @return bool
     */
    private function isThrottlingError(Throwable $exception, string $operation_type): bool
    {
        $message = strtolower(trim((string)$exception->getMessage()));
        if ($message === '') {
            return false;
        }

        $throttle_fragments = [
            'throttl',
            'too many request',
            'too many page',
            'rate limit',
            // Known EE localized wording seen in Issue #121 reports.
            'te veel pagina',
            'probeer het later nog eens'
        ];

        foreach ($throttle_fragments as $fragment) {
            if (strpos($message, $fragment) !== false) {
                return true;
            }
        }

        return false;
    }
    
    /**
     * Mark notifications as read
     * 
     * @param array $notification_ids Array of notification IDs to mark as read
     * @return bool Success status
     */
    public function markAsRead(array $notification_ids): bool
    {
        try {
            if (empty($notification_ids)) {
                return true;
            }
            
            ee()->db->where_in('id', $notification_ids)
                ->where('site_id', $this->site_id)
                ->update($this->table_name, ['is_read' => 1]);
            
            return true;
            
        } catch (Exception $e) {
            $this->utilities->debug_message(
                "Failed to mark notifications as read: " . $e->getMessage(),
                'ErrorNotificationService::markAsRead'
            );
            return false;
        }
    }

    /**
     * Mark all unread notifications as read for the current site.
     *
     * @return bool Success status
     */
    public function markAllUnreadAsRead(): bool
    {
        try {
            // Check table existence first
            if (!ee()->db->table_exists($this->table_name)) {
                return true;
            }

            ee()->db->where('site_id', $this->site_id)
                ->where('is_read', 0)
                ->update($this->table_name, ['is_read' => 1]);

            return true;

        } catch (Exception $e) {
            $this->utilities->debug_message(
                "Failed to mark all notifications as read: " . $e->getMessage(),
                'ErrorNotificationService::markAllUnreadAsRead'
            );
            return false;
        }
    }
    
    /**
     * Create a short, user-friendly error message
     * 
     * @param array $row Database row
     * @return string Short error message
     */
    private function _createShortMessage(array $row): string
    {
        $operation_type = ucfirst($row['operation_type']);
        $template = $row['template_name'] ? " in template '{$row['template_name']}'" : '';
        
        return "{$operation_type} processing failed{$template}";
    }
    
    /**
     * Determine severity level of error
     * 
     * @param array $row Database row
     * @return string Severity level (critical, error, warning)
     */
    private function _determineSeverity(array $row): string
    {
        $error_message = strtolower($row['error_message']);
        
        // Critical errors
        if (strpos($error_message, 'fatal') !== false || 
            strpos($error_message, 'memory') !== false ||
            strpos($error_message, 'timeout') !== false) {
            return 'critical';
        }
        
        // Warnings (less severe issues)
        if (strpos($error_message, 'warning') !== false ||
            strpos($error_message, 'deprecated') !== false) {
            return 'warning';
        }
        
        // Default to error
        return 'error';
    }
    
    /**
     * Extract tag parameters from context
     * 
     * @param array $context Context data
     * @return array Tag parameters
     */
    private function _extractTagParameters(array $context): array
    {
        // Look for tag parameters in various context keys
        $param_keys = ['tag_parameters', 'parameters', 'params', 'tag_params'];
        
        foreach ($param_keys as $key) {
            if (!empty($context[$key]) && is_array($context[$key])) {
                return $context[$key];
            }
        }
        
        return [];
    }
    
    /**
     * Extract template name from context
     * 
     * @param array $context Context data
     * @return string|null Template name or null
     */
    private function _extractTemplateName(array $context): ?string
    {
        // Try various context keys that might contain template info
        $template_keys = ['template_name', 'template', 'current_template', 'ee_template'];
        
        foreach ($template_keys as $key) {
            if (!empty($context[$key])) {
                return (string) $context[$key];
            }
        }
        
        // Try to get from EE template manager if available.
        // Some request contexts expose TMPL without template_group/template_name properties.
        if (isset(ee()->TMPL)) {
            $template_name = isset(ee()->TMPL->template_name) ? (string) ee()->TMPL->template_name : '';
            $template_group = isset(ee()->TMPL->template_group) ? (string) ee()->TMPL->template_group : '';

            if ($template_name !== '') {
                return $template_group !== ''
                    ? $template_group . '/' . $template_name
                    : $template_name;
            }
        }
        
        return null;
    }
    
    /**
     * Format notification data for Control Panel display
     * 
     * @param array $row Raw database row
     * @return array Formatted notification data
     */
    private function _formatNotificationForDisplay(array $row): array
    {
        return [
            'id' => (int) $row['id'],
            'operation_type' => $row['operation_type'],
            'error_message' => $row['error_message'],
            'template_name' => $row['template_name'],
            'created_at' => $row['created_at'],
            'created_at_formatted' => ee()->localize->human_time($row['created_at']),
            'tag_parameters' => json_decode($row['tag_parameters'] ?? '[]', true),
            'context_data' => json_decode($row['context_data'] ?? '[]', true),
            'severity' => $this->_determineSeverity($row),
            'short_message' => $this->_createShortMessage($row),
            'details_url' => ee('CP/URL')->make('addons/settings/jcogs_img_pro/error_details/' . $row['id'])
        ];
    }
    
    /**
     * Sanitize context data for storage
     * 
     * @param array $context Raw context data
     * @return array Sanitized context data
     */
    private function _sanitiseContext(array $context): array
    {
        // Remove sensitive data and limit size
        unset($context['password'], $context['secret'], $context['key']);
        
        // Truncate large strings
        array_walk_recursive($context, function (&$value) {
            if (is_string($value) && strlen($value) > 1000) {
                $value = substr($value, 0, 1000) . '... [truncated]';
            }
        });
        
        return $context;
    }
}