<?php

namespace DeskMoloni\Libraries;

/**
 * Descomplicar® Crescimento Digital
 * https://descomplicar.pt
 * 
 * Retry Handler
 * Advanced retry logic with exponential backoff, jitter, and circuit breaker pattern
 * 
 * @package DeskMoloni
 * @subpackage Libraries
 * @category RetryLogic
 * @author Descomplicar® - PHP Fullstack Engineer
 * @version 1.0.0
 */

defined('BASEPATH') or exit('No direct script access allowed');

use DeskMoloni\Libraries\ErrorHandler;

class RetryHandler
{
    protected $CI;
    protected $model;
    protected $error_handler;
    
    // Retry configuration
    const DEFAULT_MAX_ATTEMPTS = 5;
    const DEFAULT_BASE_DELAY = 1; // seconds
    const DEFAULT_MAX_DELAY = 300; // 5 minutes
    const DEFAULT_BACKOFF_MULTIPLIER = 2;
    const DEFAULT_JITTER_ENABLED = true;
    
    // Circuit breaker configuration
    const CIRCUIT_BREAKER_FAILURE_THRESHOLD = 10;
    const CIRCUIT_BREAKER_TIMEOUT = 300; // 5 minutes
    const CIRCUIT_BREAKER_SUCCESS_THRESHOLD = 3;
    
    // Retry strategies
    const STRATEGY_EXPONENTIAL = 'exponential';
    const STRATEGY_LINEAR = 'linear';
    const STRATEGY_FIXED = 'fixed';
    const STRATEGY_FIBONACCI = 'fibonacci';
    
    // Circuit breaker states
    const CIRCUIT_CLOSED = 'closed';
    const CIRCUIT_OPEN = 'open';
    const CIRCUIT_HALF_OPEN = 'half_open';
    
    // Retryable error types
    protected $retryable_errors = [
        'connection_timeout',
        'read_timeout',
        'network_error',
        'server_error',
        'rate_limit',
        'temporary_unavailable',
        'circuit_breaker_open'
    ];
    
    // Non-retryable error types
    protected $non_retryable_errors = [
        'authentication_failed',
        'authorization_denied',
        'invalid_data',
        'resource_not_found',
        'bad_request',
        'conflict'
    ];
    
    public function __construct()
    {
        $this->CI = &get_instance();
        $this->CI->load->model('desk_moloni_model');
        $this->model = $this->CI->desk_moloni_model;
        $this->error_handler = new ErrorHandler();
        
        log_activity('RetryHandler initialized');
    }
    
    /**
     * Calculate retry delay with exponential backoff
     * 
     * @param int $attempt_number
     * @param string $strategy
     * @param array $options
     * @return int Delay in seconds
     */
    public function calculate_retry_delay(int $attempt_number, string $strategy = self::STRATEGY_EXPONENTIAL, array $options = []): int
    {
        $base_delay = $options['base_delay'] ?? self::DEFAULT_BASE_DELAY;
        $max_delay = $options['max_delay'] ?? self::DEFAULT_MAX_DELAY;
        $multiplier = $options['multiplier'] ?? self::DEFAULT_BACKOFF_MULTIPLIER;
        $jitter_enabled = $options['jitter'] ?? self::DEFAULT_JITTER_ENABLED;
        
        switch ($strategy) {
            case self::STRATEGY_EXPONENTIAL:
                $delay = $base_delay * pow($multiplier, $attempt_number - 1);
                break;
                
            case self::STRATEGY_LINEAR:
                $delay = $base_delay * $attempt_number;
                break;
                
            case self::STRATEGY_FIXED:
                $delay = $base_delay;
                break;
                
            case self::STRATEGY_FIBONACCI:
                $delay = $this->fibonacci_delay($attempt_number, $base_delay);
                break;
                
            default:
                $delay = $base_delay * pow($multiplier, $attempt_number - 1);
        }
        
        // Cap at maximum delay
        $delay = min($delay, $max_delay);
        
        // Add jitter to prevent thundering herd
        if ($jitter_enabled) {
            $delay = $this->add_jitter($delay);
        }
        
        return (int)$delay;
    }
    
    /**
     * Determine if an error is retryable
     * 
     * @param string $error_type
     * @param string $error_message
     * @param int $http_status_code
     * @return bool
     */
    public function is_retryable_error(string $error_type, string $error_message = '', ?int $http_status_code = null): bool
    {
        // Check explicit non-retryable errors first
        if (in_array($error_type, $this->non_retryable_errors)) {
            return false;
        }
        
        // Check explicit retryable errors
        if (in_array($error_type, $this->retryable_errors)) {
            return true;
        }
        
        // Check HTTP status codes
        if ($http_status_code !== null) {
            return $this->is_retryable_http_status($http_status_code);
        }
        
        // Check error message patterns
        return $this->is_retryable_error_message($error_message);
    }
    
    /**
     * Execute operation with retry logic
     * 
     * @param callable $operation
     * @param array $retry_config
     * @param array $context
     * @return array
     */
    public function execute_with_retry(callable $operation, $retry_config = [], $context = [])
    {
        $max_attempts = $retry_config['max_attempts'] ?? self::DEFAULT_MAX_ATTEMPTS;
        $strategy = $retry_config['strategy'] ?? self::STRATEGY_EXPONENTIAL;
        $circuit_breaker_key = $context['circuit_breaker_key'] ?? null;
        
        // Check circuit breaker if enabled
        if ($circuit_breaker_key && $this->is_circuit_breaker_open($circuit_breaker_key)) {
            return [
                'success' => false,
                'message' => 'Circuit breaker is open',
                'error_type' => 'circuit_breaker_open',
                'attempts' => 0
            ];
        }
        
        $last_error = null;
        
        for ($attempt = 1; $attempt <= $max_attempts; $attempt++) {
            try {
                // Record attempt
                $this->record_retry_attempt($context, $attempt);
                
                // Execute operation
                $result = $operation($attempt);
                
                // Success - record and reset circuit breaker
                if ($result['success']) {
                    $this->record_retry_success($context, $attempt);
                    
                    if ($circuit_breaker_key) {
                        $this->record_circuit_breaker_success($circuit_breaker_key);
                    }
                    
                    return array_merge($result, ['attempts' => $attempt]);
                }
                
                $last_error = $result;
                
                // Check if error is retryable
                if (!$this->is_retryable_error(
                    $result['error_type'] ?? 'unknown',
                    $result['message'] ?? '',
                    $result['http_status'] ?? null
                )) {
                    break;
                }
                
                // Don't delay after last attempt
                if ($attempt < $max_attempts) {
                    $delay = $this->calculate_retry_delay($attempt, $strategy, $retry_config);
                    $this->record_retry_delay($context, $attempt, $delay);
                    sleep($delay);
                }
                
            } catch (\Exception $e) {
                $last_error = [
                    'success' => false,
                    'message' => $e->getMessage(),
                    'error_type' => 'exception',
                    'exception' => $e
                ];
                
                // Record exception attempt
                $this->record_retry_exception($context, $attempt, $e);
                
                // Check if exception is retryable
                if (!$this->is_retryable_exception($e)) {
                    break;
                }
                
                if ($attempt < $max_attempts) {
                    $delay = $this->calculate_retry_delay($attempt, $strategy, $retry_config);
                    sleep($delay);
                }
            }
        }
        
        // All retries failed
        $this->record_retry_failure($context, $max_attempts, $last_error);
        
        // Update circuit breaker on failure
        if ($circuit_breaker_key) {
            $this->record_circuit_breaker_failure($circuit_breaker_key);
        }
        
        return array_merge($last_error, ['attempts' => $max_attempts]);
    }
    
    /**
     * Get retry statistics for monitoring
     * 
     * @param array $filters
     * @return array
     */
    public function get_retry_statistics($filters = [])
    {
        return [
            'total_attempts' => $this->model->count_retry_attempts($filters),
            'total_successes' => $this->model->count_retry_successes($filters),
            'total_failures' => $this->model->count_retry_failures($filters),
            'success_rate' => $this->calculate_retry_success_rate($filters),
            'average_attempts' => $this->model->get_average_retry_attempts($filters),
            'retry_distribution' => $this->model->get_retry_attempt_distribution($filters),
            'error_types' => $this->model->get_retry_error_types($filters),
            'circuit_breaker_states' => $this->get_circuit_breaker_states()
        ];
    }
    
    /**
     * Check circuit breaker state
     * 
     * @param string $circuit_key
     * @return bool
     */
    public function is_circuit_breaker_open($circuit_key)
    {
        $circuit_state = $this->get_circuit_breaker_state($circuit_key);
        
        switch ($circuit_state['state']) {
            case self::CIRCUIT_OPEN:
                // Check if timeout has passed
                if (time() - $circuit_state['opened_at'] >= self::CIRCUIT_BREAKER_TIMEOUT) {
                    $this->set_circuit_breaker_state($circuit_key, self::CIRCUIT_HALF_OPEN);
                    return false;
                }
                return true;
                
            case self::CIRCUIT_HALF_OPEN:
                return false;
                
            case self::CIRCUIT_CLOSED:
            default:
                return false;
        }
    }
    
    /**
     * Record circuit breaker failure
     * 
     * @param string $circuit_key
     */
    public function record_circuit_breaker_failure($circuit_key)
    {
        $circuit_state = $this->get_circuit_breaker_state($circuit_key);
        $failure_count = $circuit_state['failure_count'] + 1;
        
        if ($failure_count >= self::CIRCUIT_BREAKER_FAILURE_THRESHOLD) {
            $this->set_circuit_breaker_state($circuit_key, self::CIRCUIT_OPEN, [
                'failure_count' => $failure_count,
                'opened_at' => time()
            ]);
            
            $this->error_handler->log_error(
                ErrorHandler::CATEGORY_SYSTEM,
                'CIRCUIT_BREAKER_OPENED',
                "Circuit breaker opened for {$circuit_key} after {$failure_count} failures",
                ['circuit_key' => $circuit_key],
                ErrorHandler::SEVERITY_HIGH
            );
        } else {
            $this->update_circuit_breaker_failure_count($circuit_key, $failure_count);
        }
    }
    
    /**
     * Record circuit breaker success
     * 
     * @param string $circuit_key
     */
    public function record_circuit_breaker_success($circuit_key)
    {
        $circuit_state = $this->get_circuit_breaker_state($circuit_key);
        
        if ($circuit_state['state'] === self::CIRCUIT_HALF_OPEN) {
            $success_count = $circuit_state['success_count'] + 1;
            
            if ($success_count >= self::CIRCUIT_BREAKER_SUCCESS_THRESHOLD) {
                $this->set_circuit_breaker_state($circuit_key, self::CIRCUIT_CLOSED, [
                    'success_count' => 0,
                    'failure_count' => 0
                ]);
                
                log_activity("Circuit breaker closed for {$circuit_key} after successful operations");
            } else {
                $this->update_circuit_breaker_success_count($circuit_key, $success_count);
            }
        } else {
            // Reset failure count on success
            $this->update_circuit_breaker_failure_count($circuit_key, 0);
        }
    }
    
    /**
     * Get optimal retry configuration for operation type
     * 
     * @param string $operation_type
     * @param string $entity_type
     * @return array
     */
    public function get_optimal_retry_config($operation_type, $entity_type = null)
    {
        $base_config = [
            'max_attempts' => self::DEFAULT_MAX_ATTEMPTS,
            'strategy' => self::STRATEGY_EXPONENTIAL,
            'base_delay' => self::DEFAULT_BASE_DELAY,
            'max_delay' => self::DEFAULT_MAX_DELAY,
            'multiplier' => self::DEFAULT_BACKOFF_MULTIPLIER,
            'jitter' => self::DEFAULT_JITTER_ENABLED
        ];
        
        // Customize based on operation type
        switch ($operation_type) {
            case 'api_call':
                $base_config['max_attempts'] = 3;
                $base_config['base_delay'] = 2;
                $base_config['max_delay'] = 60;
                break;
                
            case 'database_operation':
                $base_config['max_attempts'] = 2;
                $base_config['strategy'] = self::STRATEGY_FIXED;
                $base_config['base_delay'] = 1;
                break;
                
            case 'file_operation':
                $base_config['max_attempts'] = 3;
                $base_config['strategy'] = self::STRATEGY_LINEAR;
                $base_config['base_delay'] = 1;
                break;
                
            case 'sync_operation':
                $base_config['max_attempts'] = 5;
                $base_config['base_delay'] = 5;
                $base_config['max_delay'] = 300;
                break;
        }
        
        // Further customize based on entity type
        if ($entity_type) {
            switch ($entity_type) {
                case 'customer':
                    $base_config['max_attempts'] = min($base_config['max_attempts'], 3);
                    break;
                    
                case 'invoice':
                    $base_config['max_attempts'] = 5; // More important
                    $base_config['max_delay'] = 600;
                    break;
                    
                case 'product':
                    $base_config['max_attempts'] = 3;
                    break;
            }
        }
        
        return $base_config;
    }
    
    /**
     * Add jitter to delay to prevent thundering herd
     * 
     * @param float $delay
     * @param float $jitter_factor
     * @return float
     */
    protected function add_jitter($delay, $jitter_factor = 0.1)
    {
        $jitter_range = $delay * $jitter_factor;
        $jitter = (mt_rand() / mt_getrandmax()) * $jitter_range * 2 - $jitter_range;
        
        return max(0, $delay + $jitter);
    }
    
    /**
     * Calculate fibonacci delay
     * 
     * @param int $n
     * @param float $base_delay
     * @return float
     */
    protected function fibonacci_delay($n, $base_delay)
    {
        if ($n <= 1) return $base_delay;
        if ($n == 2) return $base_delay;
        
        $a = $base_delay;
        $b = $base_delay;
        
        for ($i = 3; $i <= $n; $i++) {
            $temp = $a + $b;
            $a = $b;
            $b = $temp;
        }
        
        return $b;
    }
    
    /**
     * Check if HTTP status code is retryable
     * 
     * @param int $status_code
     * @return bool
     */
    protected function is_retryable_http_status($status_code)
    {
        // 5xx server errors are generally retryable
        if ($status_code >= 500) {
            return true;
        }
        
        // Some 4xx errors are retryable
        $retryable_4xx = [408, 429, 423, 424]; // Request timeout, rate limit, locked, failed dependency
        
        return in_array($status_code, $retryable_4xx);
    }
    
    /**
     * Check if error message indicates retryable error
     * 
     * @param string $error_message
     * @return bool
     */
    protected function is_retryable_error_message($error_message)
    {
        $retryable_patterns = [
            '/timeout/i',
            '/connection.*failed/i',
            '/network.*error/i',
            '/temporary.*unavailable/i',
            '/service.*unavailable/i',
            '/rate.*limit/i',
            '/too many requests/i',
            '/server.*error/i'
        ];
        
        foreach ($retryable_patterns as $pattern) {
            if (preg_match($pattern, $error_message)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Check if exception is retryable
     * 
     * @param \Exception $exception
     * @return bool
     */
    protected function is_retryable_exception($exception)
    {
        $retryable_exceptions = [
            'PDOException',
            'mysqli_sql_exception',
            'RedisException',
            'cURLException',
            'TimeoutException'
        ];
        
        $exception_class = get_class($exception);
        
        return in_array($exception_class, $retryable_exceptions) ||
               $this->is_retryable_error_message($exception->getMessage());
    }
    
    /**
     * Record retry attempt
     * 
     * @param array $context
     * @param int $attempt
     */
    protected function record_retry_attempt($context, $attempt)
    {
        $this->model->record_retry_attempt([
            'operation_type' => $context['operation_type'] ?? 'unknown',
            'entity_type' => $context['entity_type'] ?? null,
            'entity_id' => $context['entity_id'] ?? null,
            'attempt_number' => $attempt,
            'attempted_at' => date('Y-m-d H:i:s'),
            'context' => json_encode($context)
        ]);
    }
    
    /**
     * Record retry success
     * 
     * @param array $context
     * @param int $total_attempts
     */
    protected function record_retry_success($context, $total_attempts)
    {
        $this->model->record_retry_success([
            'operation_type' => $context['operation_type'] ?? 'unknown',
            'entity_type' => $context['entity_type'] ?? null,
            'entity_id' => $context['entity_id'] ?? null,
            'total_attempts' => $total_attempts,
            'succeeded_at' => date('Y-m-d H:i:s'),
            'context' => json_encode($context)
        ]);
    }
    
    /**
     * Record retry failure
     * 
     * @param array $context
     * @param int $total_attempts
     * @param array $last_error
     */
    protected function record_retry_failure($context, $total_attempts, $last_error)
    {
        $this->model->record_retry_failure([
            'operation_type' => $context['operation_type'] ?? 'unknown',
            'entity_type' => $context['entity_type'] ?? null,
            'entity_id' => $context['entity_id'] ?? null,
            'total_attempts' => $total_attempts,
            'failed_at' => date('Y-m-d H:i:s'),
            'last_error' => json_encode($last_error),
            'context' => json_encode($context)
        ]);
    }
    
    /**
     * Get circuit breaker state
     * 
     * @param string $circuit_key
     * @return array
     */
    protected function get_circuit_breaker_state($circuit_key)
    {
        return $this->model->get_circuit_breaker_state($circuit_key) ?: [
            'state' => self::CIRCUIT_CLOSED,
            'failure_count' => 0,
            'success_count' => 0,
            'opened_at' => null
        ];
    }
    
    /**
     * Set circuit breaker state
     * 
     * @param string $circuit_key
     * @param string $state
     * @param array $additional_data
     */
    protected function set_circuit_breaker_state($circuit_key, $state, $additional_data = [])
    {
        $data = array_merge([
            'circuit_key' => $circuit_key,
            'state' => $state,
            'updated_at' => date('Y-m-d H:i:s')
        ], $additional_data);
        
        $this->model->set_circuit_breaker_state($circuit_key, $data);
    }
    
    /**
     * Calculate retry success rate
     * 
     * @param array $filters
     * @return float
     */
    protected function calculate_retry_success_rate($filters)
    {
        $total_attempts = $this->model->count_retry_attempts($filters);
        $total_successes = $this->model->count_retry_successes($filters);
        
        return $total_attempts > 0 ? ($total_successes / $total_attempts) * 100 : 0;
    }
    
    /**
     * Get all circuit breaker states
     * 
     * @return array
     */
    protected function get_circuit_breaker_states()
    {
        return $this->model->get_all_circuit_breaker_states();
    }
}