/**
 * Descomplicar® Crescimento Digital
 * https://descomplicar.pt
 */

<?php

/**
 * Config_model.php
 * 
 * Configuration management model for Desk-Moloni v3.0
 * Handles secure storage of API credentials, OAuth tokens, and module configuration
 * Supports encryption for sensitive data and OAuth token management with expiration
 * 
 * @package DeskMoloni\Models
 * @author PHP Fullstack Engineer  
 * @version 3.0
 */

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

require_once(dirname(__FILE__) . '/Desk_moloni_model.php');

class Config_model extends Desk_moloni_model
{
    /**
     * Table name
     */
    private $table = 'desk_moloni_config';
    
    /**
     * Configuration cache
     */
    private static $config_cache = [];
    
    /**
     * Cache TTL in seconds (5 minutes)
     */
    private $cache_ttl = 300;
    
    /**
     * Sensitive configuration keys that should be encrypted
     */
    private $sensitiveKeys = [
        'oauth_client_secret',
        'oauth_access_token', 
        'oauth_refresh_token',
        'api_key',
        'webhook_secret'
    ];
    
    /**
     * Default configuration values
     */
    private $defaultConfig = [
        'module_version' => '3.0.0',
        'api_base_url' => 'https://api.moloni.pt/v1/',
        'api_timeout' => '30',
        'sync_enabled' => '1',
        'sync_interval_minutes' => '15',
        'max_retry_attempts' => '3',
        'log_retention_days' => '365',
        'queue_batch_size' => '50',
        'encryption_algorithm' => 'AES-256-GCM'
    ];
    
    /**
     * Configuration Model Constructor
     *
     * Initializes the configuration model with proper table naming,
     * encryption setup, and default configuration initialization.
     *
     * @since 3.0.0
     * @author Descomplicar®
     * @throws Exception If table initialization fails or database connection issues
     */
    public function __construct()
    {
        parent::__construct();
        $this->table = $this->getTableName('config');
        $this->initializeDefaults();
    }
    
    /**
     * Retrieve configuration value by key with automatic decryption
     *
     * Fetches configuration value from database with automatic decryption
     * for sensitive keys. Returns default value if key doesn't exist.
     *
     * @param string $key Configuration key to retrieve
     * @param mixed $default Default value returned if key is not found
     * @return mixed Configuration value (decrypted if encrypted) or default value
     * @throws Exception When database query fails or decryption errors
     * @since 3.0.0
     * @author Descomplicar®
     */
    public function get($key, $default = null)
    {
        try {
            // Validate key
            if (empty($key)) {
                return $default;
            }
            
            $query = $this->db->where('setting_key', $key)->get($this->table);
            
            if ($query->num_rows() === 0) {
                return $default;
            }
            
            $row = $query->row();
            
            // Decrypt if encrypted
            if ($row->encrypted == 1) {
                return $this->decryptData($row->setting_value);
            }
            
            return $row->setting_value;
            
        } catch (Exception $e) {
            log_message('error', 'Desk-Moloni config get error: ' . $e->getMessage());
            return $default;
        }
    }
    
    /**
     * Store configuration value with automatic encryption for sensitive keys
     *
     * Saves configuration value to database with automatic encryption detection
     * for sensitive keys, comprehensive validation, and secure storage.
     *
     * @param string $key Configuration key (must be non-empty, alphanumeric with underscores)
     * @param mixed $value Configuration value to store
     * @param bool $forceEncryption Force encryption regardless of automatic detection
     * @return bool True on successful save, false on failure
     * @throws InvalidArgumentException When key validation fails or invalid parameters
     * @throws Exception When database operations fail or encryption errors
     * @since 3.0.0
     * @author Descomplicar®
     */
    public function set($key, $value, $forceEncryption = false)
    {
        try {
            // Validate key
            if (empty($key)) {
                throw new InvalidArgumentException('Configuration key cannot be empty');
            }
            
            // Validate input
            $validationErrors = $this->validateConfigData(['setting_key' => $key, 'setting_value' => $value]);
            if (!empty($validationErrors)) {
                throw new Exception('Validation failed: ' . implode(', ', $validationErrors));
            }
            
            // Determine if value should be encrypted
            $shouldEncrypt = $forceEncryption || $this->isSensitiveKey($key);
            
            // Prepare data
            $data = [
                'setting_key' => $key,
                'setting_value' => $shouldEncrypt ? $this->encryptData($value) : $value,
                'encrypted' => $shouldEncrypt ? 1 : 0,
                'updated_at' => date('Y-m-d H:i:s')
            ];
            
            // Check if key exists
            $existing = $this->db->where('setting_key', $key)->get($this->table);
            
            if ($existing->num_rows() > 0) {
                // Update existing
                $result = $this->db->where('setting_key', $key)->update($this->table, $data);
                $this->logDatabaseOperation('update', $this->table, $data, $existing->row()->id);
            } else {
                // Insert new
                $data['created_at'] = date('Y-m-d H:i:s');
                $result = $this->db->insert($this->table, $data);
                $this->logDatabaseOperation('create', $this->table, $data, $this->db->insert_id());
            }
            
            return $result;
            
        } catch (InvalidArgumentException $e) {
            throw $e; // Re-throw validation exceptions
        } catch (Exception $e) {
            log_message('error', 'Desk-Moloni config set error: ' . $e->getMessage());
            return false;
        }
    }
    
    /**
     * Store configuration value with forced encryption
     *
     * Convenience method for storing configuration values with mandatory encryption,
     * regardless of key type. Used for storing sensitive data securely.
     *
     * @param string $key Configuration key to store
     * @param mixed $value Configuration value to encrypt and store
     * @return bool True on successful encrypted storage, false on failure
     * @throws InvalidArgumentException When key validation fails
     * @throws Exception When encryption or database operations fail
     * @since 3.0.0
     * @author Descomplicar®
     */
    public function set_encrypted($key, $value)
    {
        return $this->set($key, $value, true);
    }
    
    /**
     * Get encrypted configuration value (decrypted)
     * 
     * @param string $key Configuration key
     * @param mixed $default Default value if key not found
     * @return mixed Decrypted configuration value
     */
    public function get_encrypted($key, $default = null)
    {
        return $this->get($key, $default);
    }
    
    /**
     * Store OAuth access token with expiration tracking
     *
     * Securely stores OAuth access token with encrypted storage and
     * expiration timestamp for automatic token refresh management.
     *
     * @param string $token OAuth access token to store securely
     * @param int $expires_at Unix timestamp when token expires
     * @return bool True on successful storage of both token and expiration, false on failure
     * @throws Exception When token encryption fails or database operations error
     * @since 3.0.0
     * @author Descomplicar®
     */
    public function set_oauth_token($token, $expires_at)
    {
        try {
            $success = true;
            $success &= $this->set('oauth_access_token', $token, true);
            $success &= $this->set('oauth_token_expires_at', date('Y-m-d H:i:s', $expires_at));
            
            return $success;
            
        } catch (Exception $e) {
            log_message('error', 'Desk-Moloni OAuth token set error: ' . $e->getMessage());
            return false;
        }
    }
    
    /**
     * Get OAuth token with metadata
     * 
     * @return array Token data array
     */
    public function get_oauth_token()
    {
        try {
            $token = $this->get('oauth_access_token');
            $expires_at = $this->get('oauth_token_expires_at');
            
            if (empty($token)) {
                return [];
            }
            
            return [
                'token' => $token,
                'expires_at' => $expires_at ? strtotime($expires_at) : null,
                'created_at' => time(),
                'type' => 'oauth_token'
            ];
            
        } catch (Exception $e) {
            log_message('error', 'Desk-Moloni OAuth token get error: ' . $e->getMessage());
            return [];
        }
    }
    
    /**
     * Validate OAuth token existence and expiration status
     *
     * Checks if OAuth access token exists and is not expired, with a
     * 5-minute buffer to prevent token expiration during API calls.
     *
     * @return bool True if token exists and is valid (not expired), false otherwise
     * @throws Exception When token validation process fails or database errors occur
     * @since 3.0.0
     * @author Descomplicar®
     */
    public function is_oauth_token_valid()
    {
        try {
            $accessToken = $this->get('oauth_access_token');
            $expiresAt = $this->get('oauth_token_expires_at');
            
            if (empty($accessToken) || empty($expiresAt)) {
                return false;
            }
            
            // Check if token is expired (with 5-minute buffer)
            $expirationTime = strtotime($expiresAt) - 300; // 5 minutes buffer
            return time() < $expirationTime;
            
        } catch (Exception $e) {
            log_message('error', 'Desk-Moloni OAuth validation error: ' . $e->getMessage());
            return false;
        }
    }
    
    /**
     * Delete configuration key
     * 
     * @param string $key Configuration key
     * @return bool Success status
     */
    public function delete($key)
    {
        try {
            if (empty($key)) {
                return false;
            }
            
            $existing = $this->db->where('setting_key', $key)->get($this->table);
            
            if ($existing->num_rows() === 0) {
                return true; // Already doesn't exist
            }
            
            $result = $this->db->where('setting_key', $key)->delete($this->table);
            $this->logDatabaseOperation('delete', $this->table, ['setting_key' => $key], $existing->row()->id);
            
            return $result;
            
        } catch (Exception $e) {
            log_message('error', 'Desk-Moloni config delete error: ' . $e->getMessage());
            return false;
        }
    }
    
    /**
     * Retrieve all configuration values with optional encryption handling
     *
     * Fetches complete configuration dataset with optional decryption of sensitive values,
     * includes default configuration values for missing keys.
     *
     * @param bool $includeEncrypted Whether to decrypt and include encrypted values (default: true)
     * @return array Complete configuration array with all keys and values,
     *               encrypted values are decrypted if $includeEncrypted is true
     * @throws Exception When database query fails or decryption errors occur
     * @since 3.0.0
     * @author Descomplicar®
     */
    public function get_all($includeEncrypted = true)
    {
        try {
            $query = $this->db->get($this->table);
            $config = [];
            
            foreach ($query->result() as $row) {
                if ($row->encrypted == 1 && $includeEncrypted) {
                    $config[$row->setting_key] = $this->decryptData($row->setting_value);
                } elseif ($row->encrypted == 0) {
                    $config[$row->setting_key] = $row->setting_value;
                }
                // Skip encrypted values if includeEncrypted is false
            }
            
            // Add default values for missing keys
            foreach ($this->defaultConfig as $key => $defaultValue) {
                if (!isset($config[$key])) {
                    $config[$key] = $defaultValue;
                }
            }
            
            return $config;
            
        } catch (Exception $e) {
            log_message('error', 'Desk-Moloni config get_all error: ' . $e->getMessage());
            return $this->defaultConfig;
        }
    }
    
    /**
     * Set multiple configuration values in batch
     * 
     * @param array $config_batch Array of key => value pairs
     * @return bool Success status
     */
    public function set_batch($config_batch)
    {
        try {
            if (!is_array($config_batch) || empty($config_batch)) {
                return false;
            }
            
            $success = true;
            
            // Use transaction for batch operations
            return $this->executeTransaction(function() use ($config_batch, &$success) {
                foreach ($config_batch as $key => $value) {
                    if (!$this->set($key, $value)) {
                        $success = false;
                        throw new Exception("Failed to set config key: {$key}");
                    }
                }
                return $success;
            });
            
        } catch (Exception $e) {
            log_message('error', 'Desk-Moloni config set_batch error: ' . $e->getMessage());
            return false;
        }
    }
    
    /**
     * Get configuration keys by pattern
     * 
     * @param string $pattern LIKE pattern for key matching
     * @param bool $includeEncrypted Whether to decrypt encrypted values
     * @return array Matching configuration
     */
    public function getByPattern($pattern, $includeEncrypted = true)
    {
        try {
            $query = $this->db->like('setting_key', $pattern)->get($this->table);
            $config = [];
            
            foreach ($query->result() as $row) {
                if ($row->encrypted == 1 && $includeEncrypted) {
                    $config[$row->setting_key] = $this->decryptData($row->setting_value);
                } elseif ($row->encrypted == 0) {
                    $config[$row->setting_key] = $row->setting_value;
                }
            }
            
            return $config;
            
        } catch (Exception $e) {
            log_message('error', 'Desk-Moloni config getByPattern error: ' . $e->getMessage());
            return [];
        }
    }
    
    /**
     * Get OAuth configuration
     * 
     * @return array OAuth configuration
     */
    public function getOAuthConfig()
    {
        $oauthKeys = [
            'oauth_client_id',
            'oauth_client_secret', 
            'oauth_access_token',
            'oauth_refresh_token',
            'oauth_token_expires_at'
        ];
        
        $config = [];
        foreach ($oauthKeys as $key) {
            $config[$key] = $this->get($key);
        }
        
        return $config;
    }
    
    /**
     * Set OAuth tokens
     * 
     * @param array $tokens OAuth token data
     * @return bool Success status
     */
    public function setOAuthTokens($tokens)
    {
        try {
            $requiredTokens = ['access_token', 'refresh_token', 'expires_in'];
            
            foreach ($requiredTokens as $required) {
                if (!isset($tokens[$required])) {
                    throw new Exception("Missing required OAuth token: {$required}");
                }
            }
            
            // Calculate expiration timestamp
            $expiresAt = date('Y-m-d H:i:s', time() + (int)$tokens['expires_in']);
            
            $success = true;
            $success &= $this->set('oauth_access_token', $tokens['access_token'], true);
            $success &= $this->set('oauth_refresh_token', $tokens['refresh_token'], true);
            $success &= $this->set('oauth_token_expires_at', $expiresAt);
            
            return $success;
            
        } catch (Exception $e) {
            log_message('error', 'Desk-Moloni OAuth tokens error: ' . $e->getMessage());
            return false;
        }
    }
    
    /**
     * Clear all OAuth tokens (for logout/revoke)
     * 
     * @return bool Success status
     */
    public function clearOAuthTokens()
    {
        $success = true;
        $oauthKeys = ['oauth_access_token', 'oauth_refresh_token', 'oauth_token_expires_at'];
        
        foreach ($oauthKeys as $key) {
            $success &= $this->delete($key);
        }
        
        return $success;
    }
    
    /**
     * Get API configuration
     * 
     * @return array API configuration
     */
    public function getAPIConfig()
    {
        return [
            'base_url' => $this->get('api_base_url', 'https://api.moloni.pt/v1/'),
            'timeout' => (int)$this->get('api_timeout', 30),
            'company_id' => $this->get('moloni_company_id'),
            'access_token' => $this->get('oauth_access_token')
        ];
    }
    
    /**
     * Check if configuration key is sensitive and should be encrypted
     * 
     * @param string $key Configuration key
     * @return bool True if key is sensitive
     */
    private function isSensitiveKey($key)
    {
        return in_array($key, $this->sensitiveKeys) || 
               strpos($key, 'password') !== false ||
               strpos($key, 'secret') !== false ||
               strpos($key, 'token') !== false ||
               strpos($key, 'key') !== false;
    }
    
    /**
     * Validate configuration data
     * 
     * @param array $data Configuration data to validate
     * @return array Validation errors
     */
    private function validateConfigData($data)
    {
        $errors = [];
        
        // Required fields
        $requiredFields = ['setting_key'];
        $errors = array_merge($errors, $this->validateRequiredFields($data, $requiredFields));
        
        // Field length limits
        $fieldLimits = [
            'setting_key' => 255
        ];
        $errors = array_merge($errors, $this->validateFieldLengths($data, $fieldLimits));
        
        // Key format validation
        if (isset($data['setting_key'])) {
            if (!preg_match('/^[a-z0-9_]+$/', $data['setting_key'])) {
                $errors[] = 'Setting key must contain only lowercase letters, numbers, and underscores';
            }
        }
        
        return $errors;
    }
    
    /**
     * Initialize default configuration values in database
     *
     * Sets up default configuration values for module operation,
     * only creates values that don't already exist in database.
     *
     * @return bool True if all default values were successfully initialized, false on any failure
     * @throws Exception When database operations fail or default value validation errors
     * @since 3.0.0
     * @author Descomplicar®
     */
    public function initializeDefaults()
    {
        $success = true;
        
        foreach ($this->defaultConfig as $key => $value) {
            // Only set if not already exists
            if ($this->get($key) === null) {
                $success &= $this->set($key, $value);
            }
        }
        
        return $success;
    }
    
    /**
     * Export configuration for backup (excluding sensitive data)
     * 
     * @return array Non-sensitive configuration data
     */
    public function exportConfig()
    {
        try {
            $allConfig = $this->get_all(false); // Don't include encrypted values
            
            // Remove sensitive keys entirely from export
            foreach ($this->sensitiveKeys as $sensitiveKey) {
                unset($allConfig[$sensitiveKey]);
            }
            
            return $allConfig;
            
        } catch (Exception $e) {
            log_message('error', 'Desk-Moloni config export error: ' . $e->getMessage());
            return [];
        }
    }
    
    /**
     * Get configuration value with caching
     * 
     * @param string $key Configuration key
     * @param mixed $default Default value if key not found
     * @return mixed Configuration value
     */
    public function get_cached($key, $default = null)
    {
        $cache_key = 'config_' . $key;
        
        // Check if cached and not expired
        if (isset(self::$config_cache[$cache_key])) {
            $cached = self::$config_cache[$cache_key];
            if ((time() - $cached['timestamp']) < $this->cache_ttl) {
                desk_moloni_log('debug', "Config cache hit for key: $key", [], 'cache');
                return $cached['value'];
            } else {
                // Cache expired, remove it
                unset(self::$config_cache[$cache_key]);
                desk_moloni_log('debug', "Config cache expired for key: $key", [], 'cache');
            }
        }
        
        // Cache miss, get from database
        $value = $this->get($key, $default);
        
        // Cache the result
        self::$config_cache[$cache_key] = [
            'value' => $value,
            'timestamp' => time()
        ];
        
        desk_moloni_log('debug', "Config cached for key: $key", ['ttl' => $this->cache_ttl], 'cache');
        return $value;
    }
    
    /**
     * Set configuration value and invalidate cache
     * 
     * @param string $key Configuration key
     * @param mixed $value Configuration value
     * @param bool $forceEncryption Force encryption even for non-sensitive keys
     * @return bool Success status
     */
    public function set_cached($key, $value, $forceEncryption = false)
    {
        $result = $this->set($key, $value, $forceEncryption);
        
        if ($result) {
            // Invalidate cache for this key
            $cache_key = 'config_' . $key;
            unset(self::$config_cache[$cache_key]);
            desk_moloni_log('debug', "Config cache invalidated for key: $key", [], 'cache');
        }
        
        return $result;
    }
    
    /**
     * Clear all configuration cache
     */
    public function clear_cache()
    {
        $count = count(self::$config_cache);
        self::$config_cache = [];
        desk_moloni_log('info', "Configuration cache cleared", ['cached_items' => $count], 'cache');
    }
    
    /**
     * Get cache statistics
     * 
     * @return array Cache statistics
     */
    public function get_cache_stats()
    {
        $stats = [
            'cached_items' => count(self::$config_cache),
            'cache_ttl' => $this->cache_ttl,
            'items' => []
        ];
        
        foreach (self::$config_cache as $key => $data) {
            $age = time() - $data['timestamp'];
            $stats['items'][] = [
                'key' => $key,
                'age_seconds' => $age,
                'expires_in' => $this->cache_ttl - $age
            ];
        }
        
        return $stats;
    }
}