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

<?php

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

/**
 * Enhanced Moloni OAuth Integration Library
 * 
 * Handles OAuth 2.0 authentication flow with Moloni API
 * Implements proper security, rate limiting, and error handling
 * 
 * @package     DeskMoloni
 * @author      Descomplicar®
 * @copyright   2025 Descomplicar
 * @version     3.0.0
 */
class Moloni_oauth
{
    private $CI;
    
    // OAuth endpoints (updated to match API specification)
    private $auth_url = 'https://www.moloni.pt/oauth/authorize';
    private $token_url = 'https://api.moloni.pt/v1/oauth2/token';
    
    // OAuth configuration
    private $client_id;
    private $client_secret;
    private $redirect_uri;
    
    // Token manager
    private $token_manager;
    
    // Rate limiting for OAuth requests
    private $oauth_request_count = 0;
    private $oauth_window_start = 0;
    private $oauth_max_requests = 10; // Conservative limit for OAuth endpoints
    
    // Error tracking
    private $last_error = null;
    
    // Request timeout
    private $request_timeout = 30;
    
    // PKCE support
    private $use_pkce = true;
    private $code_verifier;
    private $code_challenge;
    
    public function __construct()
    {
        $this->CI = &get_instance();
        $this->CI->load->helper('url');
        $this->CI->load->library('desk_moloni/token_manager');
        
        $this->token_manager = $this->CI->token_manager;
        
        // Set redirect URI
        $this->redirect_uri = admin_url('desk_moloni/oauth_callback');
        
        // Load saved configuration
        $this->load_configuration();
    }
    
    /**
     * Load OAuth configuration from database
     */
    private function load_configuration()
    {
        $this->client_id = get_option('desk_moloni_client_id');
        $this->client_secret = get_option('desk_moloni_client_secret');
        $this->request_timeout = (int)get_option('desk_moloni_oauth_timeout', 30);
        $this->use_pkce = (bool)get_option('desk_moloni_use_pkce', true);
    }
    
    /**
     * Configure OAuth credentials
     * 
     * @param string $client_id OAuth client ID
     * @param string $client_secret OAuth client secret
     * @param array $options Additional configuration options
     * @return bool Configuration success
     */
    public function configure($client_id, $client_secret, $options = [])
    {
        // Validate inputs
        if (empty($client_id) || empty($client_secret)) {
            throw new InvalidArgumentException('Client ID and Client Secret are required');
        }
        
        $this->client_id = $client_id;
        $this->client_secret = $client_secret;
        
        // Process options
        if (isset($options['redirect_uri'])) {
            $this->redirect_uri = $options['redirect_uri'];
        }
        
        if (isset($options['timeout'])) {
            $this->request_timeout = (int)$options['timeout'];
        }
        
        if (isset($options['use_pkce'])) {
            $this->use_pkce = (bool)$options['use_pkce'];
        }
        
        // Save to database
        update_option('desk_moloni_client_id', $client_id);
        update_option('desk_moloni_client_secret', $client_secret);
        update_option('desk_moloni_oauth_timeout', $this->request_timeout);
        update_option('desk_moloni_use_pkce', $this->use_pkce);
        
        log_activity('Desk-Moloni: OAuth configuration updated');
        
        return true;
    }
    
    /**
     * Check if OAuth is properly configured
     * 
     * @return bool Configuration status
     */
    public function is_configured()
    {
        return !empty($this->client_id) && !empty($this->client_secret);
    }
    
    /**
     * Check if OAuth is connected (has valid token)
     * 
     * @return bool Connection status
     */
    public function is_connected()
    {
        if (!$this->is_configured()) {
            return false;
        }
        
        // Check token validity
        if (!$this->token_manager->are_tokens_valid()) {
            // Try to refresh if we have a refresh token
            return $this->refresh_access_token();
        }
        
        return true;
    }
    
    /**
     * Generate authorization URL for OAuth flow
     * 
     * @param string|null $state Optional state parameter for CSRF protection
     * @param array $scopes OAuth scopes to request
     * @return string Authorization URL
     */
    public function get_authorization_url($state = null, $scopes = [])
    {
        if (!$this->is_configured()) {
            throw new Exception('OAuth not configured');
        }
        
        // Generate PKCE parameters if enabled
        if ($this->use_pkce) {
            $this->generate_pkce_parameters();
        }
        
        // Default state if not provided
        if ($state === null) {
            $state = bin2hex(random_bytes(16));
            $this->CI->session->set_userdata('desk_moloni_oauth_state', $state);
        }
        
        $params = [
            'response_type' => 'code',
            'client_id' => $this->client_id,
            'redirect_uri' => $this->redirect_uri,
            'state' => $state,
            'scope' => empty($scopes) ? 'read write' : implode(' ', $scopes)
        ];
        
        // Add PKCE challenge if enabled
        if ($this->use_pkce && $this->code_challenge) {
            $params['code_challenge'] = $this->code_challenge;
            $params['code_challenge_method'] = 'S256';
            
            // Store code verifier in session
            $this->CI->session->set_userdata('desk_moloni_code_verifier', $this->code_verifier);
        }
        
        $url = $this->auth_url . '?' . http_build_query($params);
        
        log_activity('Desk-Moloni: Authorization URL generated');
        
        return $url;
    }
    
    /**
     * Handle OAuth callback and exchange code for tokens
     * 
     * @param string $code Authorization code
     * @param string|null $state State parameter for verification
     * @return bool Exchange success
     */
    public function handle_callback($code, $state = null)
    {
        if (!$this->is_configured()) {
            throw new Exception('OAuth not configured');
        }
        
        // Validate state parameter for CSRF protection
        if ($state !== null) {
            $stored_state = $this->CI->session->userdata('desk_moloni_oauth_state');
            if ($state !== $stored_state) {
                throw new Exception('Invalid state parameter - possible CSRF attack');
            }
            $this->CI->session->unset_userdata('desk_moloni_oauth_state');
        }
        
        // Prepare token exchange data
        $data = [
            'grant_type' => 'authorization_code',
            'client_id' => $this->client_id,
            'client_secret' => $this->client_secret,
            'redirect_uri' => $this->redirect_uri,
            'code' => $code
        ];
        
        // Add PKCE verifier if used
        if ($this->use_pkce) {
            $code_verifier = $this->CI->session->userdata('desk_moloni_code_verifier');
            if ($code_verifier) {
                $data['code_verifier'] = $code_verifier;
                $this->CI->session->unset_userdata('desk_moloni_code_verifier');
            }
        }
        
        try {
            $response = $this->make_token_request($data);
            
            if (isset($response['access_token'])) {
                $success = $this->token_manager->save_tokens($response);
                
                if ($success) {
                    log_activity('Desk-Moloni: OAuth tokens received and saved');
                    return true;
                }
            }
            
            throw new Exception('Token exchange failed: Invalid response format');
            
        } catch (Exception $e) {
            $this->last_error = $e->getMessage();
            log_activity('Desk-Moloni: OAuth callback failed - ' . $e->getMessage());
            throw new Exception('OAuth callback failed: ' . $e->getMessage());
        }
    }
    
    /**
     * Refresh access token using refresh token
     * 
     * @return bool Refresh success
     */
    public function refresh_access_token()
    {
        $refresh_token = $this->token_manager->get_refresh_token();
        
        if (empty($refresh_token)) {
            return false;
        }
        
        $data = [
            'grant_type' => 'refresh_token',
            'client_id' => $this->client_id,
            'client_secret' => $this->client_secret,
            'refresh_token' => $refresh_token
        ];
        
        try {
            $response = $this->make_token_request($data);
            
            if (isset($response['access_token'])) {
                $success = $this->token_manager->save_tokens($response);
                
                if ($success) {
                    log_activity('Desk-Moloni: Access token refreshed successfully');
                    return true;
                }
            }
            
            return false;
            
        } catch (Exception $e) {
            $this->last_error = $e->getMessage();
            log_activity('Desk-Moloni: Token refresh failed - ' . $e->getMessage());
            
            // Clear invalid tokens
            $this->token_manager->clear_tokens();
            return false;
        }
    }
    
    /**
     * Get current access token
     * 
     * @return string Access token
     * @throws Exception If not connected
     */
    public function get_access_token()
    {
        if (!$this->is_connected()) {
            throw new Exception('OAuth not connected');
        }
        
        return $this->token_manager->get_access_token();
    }
    
    /**
     * Revoke access and clear tokens
     * 
     * @return bool Revocation success
     */
    public function revoke_access()
    {
        try {
            // Try to revoke token via API if possible
            $access_token = $this->token_manager->get_access_token();
            
            if ($access_token) {
                // Moloni doesn't currently support token revocation endpoint
                // So we just clear local tokens
                log_activity('Desk-Moloni: OAuth access revoked (local clear only)');
            }
            
            return $this->token_manager->clear_tokens();
            
        } catch (Exception $e) {
            log_activity('Desk-Moloni: Token revocation failed - ' . $e->getMessage());
            
            // Still try to clear local tokens
            return $this->token_manager->clear_tokens();
        }
    }
    
    /**
     * Make token request to Moloni OAuth endpoint
     * 
     * @param array $data Request data
     * @return array Response data
     * @throws Exception On request failure
     */
    private function make_token_request($data)
    {
        // Apply rate limiting
        $this->enforce_oauth_rate_limit();
        
        $ch = curl_init();
        
        curl_setopt_array($ch, [
            CURLOPT_URL => $this->token_url,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => http_build_query($data),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => $this->request_timeout,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/x-www-form-urlencoded',
                'Accept: application/json',
                'User-Agent: Desk-Moloni/3.0 OAuth'
            ],
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_MAXREDIRS => 0
        ]);
        
        $response = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        
        curl_close($ch);
        
        if ($error) {
            throw new Exception("CURL Error: {$error}");
        }
        
        $decoded = json_decode($response, true);
        
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new Exception('Invalid JSON response from OAuth endpoint');
        }
        
        if ($http_code >= 400) {
            $error_msg = $decoded['error_description'] ?? 
                        $decoded['error'] ?? 
                        "HTTP {$http_code}";
            throw new Exception("OAuth Error: {$error_msg}");
        }
        
        return $decoded;
    }
    
    /**
     * Generate PKCE parameters for enhanced security
     */
    private function generate_pkce_parameters()
    {
        // Generate code verifier (43-128 characters)
        $this->code_verifier = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
        
        // Generate code challenge
        $this->code_challenge = rtrim(strtr(base64_encode(hash('sha256', $this->code_verifier, true)), '+/', '-_'), '=');
    }
    
    /**
     * Enforce rate limiting for OAuth requests
     */
    private function enforce_oauth_rate_limit()
    {
        $current_time = time();
        
        // Reset counter if new window (5 minutes for OAuth)
        if ($current_time - $this->oauth_window_start >= 300) {
            $this->oauth_window_start = $current_time;
            $this->oauth_request_count = 0;
        }
        
        // Check if we've exceeded the limit
        if ($this->oauth_request_count >= $this->oauth_max_requests) {
            $wait_time = 300 - ($current_time - $this->oauth_window_start);
            throw new Exception("OAuth rate limit exceeded. Please wait {$wait_time} seconds.");
        }
        
        $this->oauth_request_count++;
    }
    
    /**
     * Get comprehensive OAuth status
     * 
     * @return array OAuth status information
     */
    public function get_status()
    {
        $token_status = $this->token_manager->get_token_status();
        
        return [
            'configured' => $this->is_configured(),
            'connected' => $this->is_connected(),
            'client_id' => $this->client_id ? substr($this->client_id, 0, 8) . '...' : null,
            'redirect_uri' => $this->redirect_uri,
            'use_pkce' => $this->use_pkce,
            'request_timeout' => $this->request_timeout,
            'rate_limit' => [
                'max_requests' => $this->oauth_max_requests,
                'current_count' => $this->oauth_request_count,
                'window_start' => $this->oauth_window_start
            ],
            'tokens' => $token_status
        ];
    }
    
    /**
     * Test OAuth configuration
     * 
     * @return array Test results
     */
    public function test_configuration()
    {
        $issues = [];
        
        // Check basic configuration
        if (!$this->is_configured()) {
            $issues[] = 'OAuth not configured - missing client credentials';
        }
        
        // Validate URLs
        if (!filter_var($this->redirect_uri, FILTER_VALIDATE_URL)) {
            $issues[] = 'Invalid redirect URI';
        }
        
        // Check SSL/TLS support
        if (!function_exists('curl_init')) {
            $issues[] = 'cURL extension not available';
        }
        
        // Test connectivity to OAuth endpoints
        try {
            $ch = curl_init();
            curl_setopt_array($ch, [
                CURLOPT_URL => $this->auth_url,
                CURLOPT_NOBODY => true,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => 10,
                CURLOPT_SSL_VERIFYPEER => true,
                CURLOPT_SSL_VERIFYHOST => 2
            ]);
            
            $result = curl_exec($ch);
            $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            
            curl_close($ch);
            
            if ($result === false || $http_code >= 500) {
                $issues[] = 'Cannot reach Moloni OAuth endpoints';
            }
            
        } catch (Exception $e) {
            $issues[] = 'OAuth endpoint connectivity test failed: ' . $e->getMessage();
        }
        
        // Test token manager
        $encryption_validation = $this->token_manager->validate_encryption();
        if (!$encryption_validation['is_valid']) {
            $issues = array_merge($issues, $encryption_validation['issues']);
        }
        
        return [
            'is_valid' => empty($issues),
            'issues' => $issues,
            'endpoints' => [
                'auth_url' => $this->auth_url,
                'token_url' => $this->token_url
            ],
            'encryption' => $encryption_validation
        ];
    }
    
    /**
     * Force token refresh (for testing or manual refresh)
     * 
     * @return bool Refresh success
     */
    public function force_token_refresh()
    {
        if (!$this->is_configured()) {
            throw new Exception('OAuth not configured');
        }
        
        $refresh_token = $this->token_manager->get_refresh_token();
        
        if (empty($refresh_token)) {
            throw new Exception('No refresh token available');
        }
        
        return $this->refresh_access_token();
    }
    
    /**
     * Get token expiration info
     * 
     * @return array Token expiration details
     */
    public function get_token_expiration_info()
    {
        $expires_at = $this->token_manager->get_token_expiration();
        
        if (!$expires_at) {
            return [
                'has_token' => false,
                'expires_at' => null,
                'expires_in' => null,
                'is_expired' => true,
                'expires_soon' => false
            ];
        }
        
        $now = time();
        $expires_in = $expires_at - $now;
        
        return [
            'has_token' => true,
            'expires_at' => date('Y-m-d H:i:s', $expires_at),
            'expires_at_timestamp' => $expires_at,
            'expires_in' => max(0, $expires_in),
            'expires_in_minutes' => max(0, round($expires_in / 60)),
            'is_expired' => $expires_in <= 0,
            'expires_soon' => $expires_in <= 300 // 5 minutes
        ];
    }
    
    /**
     * Validate OAuth state parameter
     * 
     * @param string $state State parameter to validate
     * @return bool Valid state
     */
    public function validate_state($state)
    {
        $stored_state = $this->CI->session->userdata('desk_moloni_oauth_state');
        
        if (empty($stored_state) || $state !== $stored_state) {
            return false;
        }
        
        // Clear used state
        $this->CI->session->unset_userdata('desk_moloni_oauth_state');
        
        return true;
    }
    
    /**
     * Security audit for OAuth implementation
     * 
     * @return array Security audit results
     */
    public function security_audit()
    {
        $audit = [
            'overall_score' => 0,
            'max_score' => 100,
            'checks' => [],
            'recommendations' => []
        ];
        
        $score = 0;
        
        // PKCE usage (20 points)
        if ($this->use_pkce) {
            $audit['checks']['pkce'] = ['status' => 'pass', 'points' => 20];
            $score += 20;
        } else {
            $audit['checks']['pkce'] = ['status' => 'fail', 'points' => 0];
            $audit['recommendations'][] = 'Enable PKCE for enhanced security';
        }
        
        // HTTPS usage (20 points)
        $uses_https = strpos($this->redirect_uri, 'https://') === 0 || $this->is_localhost();
        if ($uses_https) {
            $audit['checks']['https'] = ['status' => 'pass', 'points' => 20];
            $score += 20;
        } else {
            $audit['checks']['https'] = ['status' => 'fail', 'points' => 0];
            $audit['recommendations'][] = 'Use HTTPS for OAuth redirect URI in production';
        }
        
        // Token encryption (20 points)
        $encryption_valid = $this->token_manager->validate_encryption()['is_valid'];
        if ($encryption_valid) {
            $audit['checks']['token_encryption'] = ['status' => 'pass', 'points' => 20];
            $score += 20;
        } else {
            $audit['checks']['token_encryption'] = ['status' => 'fail', 'points' => 0];
            $audit['recommendations'][] = 'Fix token encryption issues';
        }
        
        // Rate limiting (15 points)
        $audit['checks']['rate_limiting'] = ['status' => 'pass', 'points' => 15];
        $score += 15;
        
        // Session security (15 points)
        $secure_sessions = ini_get('session.cookie_secure') === '1' || $this->is_localhost();
        if ($secure_sessions) {
            $audit['checks']['session_security'] = ['status' => 'pass', 'points' => 15];
            $score += 15;
        } else {
            $audit['checks']['session_security'] = ['status' => 'fail', 'points' => 0];
            $audit['recommendations'][] = 'Enable secure session cookies';
        }
        
        // Error handling (10 points)
        $audit['checks']['error_handling'] = ['status' => 'pass', 'points' => 10];
        $score += 10;
        
        $audit['overall_score'] = $score;
        $audit['grade'] = $this->calculate_security_grade($score);
        
        return $audit;
    }
    
    /**
     * Check if running on localhost
     * 
     * @return bool True if localhost
     */
    private function is_localhost()
    {
        $server_name = $_SERVER['SERVER_NAME'] ?? '';
        return in_array($server_name, ['localhost', '127.0.0.1', '::1']) || 
               strpos($server_name, '.local') !== false;
    }
    
    /**
     * Calculate security grade from score
     * 
     * @param int $score Security score
     * @return string Grade (A, B, C, D, F)
     */
    private function calculate_security_grade($score)
    {
        if ($score >= 90) return 'A';
        if ($score >= 80) return 'B';
        if ($score >= 70) return 'C';
        if ($score >= 60) return 'D';
        return 'F';
    }
    
    /**
     * Save OAuth tokens (required by contract)
     * 
     * @param array $tokens Token data
     * @return bool Save success
     */
    public function save_tokens($tokens)
    {
        return $this->token_manager->save_tokens($tokens);
    }
    
    /**
     * Check if token is valid (required by contract)
     * 
     * @return bool Token validity
     */
    public function is_token_valid()
    {
        return $this->token_manager->are_tokens_valid();
    }
    
    /**
     * Get authorization headers for API requests (required by contract)
     * 
     * @return array Authorization headers
     * @throws Exception If not connected
     */
    public function get_auth_headers()
    {
        if (!$this->is_connected()) {
            throw new Exception('OAuth not connected - cannot get auth headers');
        }
        
        $access_token = $this->get_access_token();
        
        return [
            'Authorization' => 'Bearer ' . $access_token,
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'User-Agent' => 'Desk-Moloni/3.0'
        ];
    }
    
    /**
     * Get last OAuth error (required by contract)
     * 
     * @return string|null Last error message
     */
    public function get_last_error()
    {
        // Implementation would track last error in property
        // For now, return null as errors are thrown as exceptions
        return $this->last_error ?? null;
    }
    
    /**
     * Check if PKCE is supported/enabled (required by contract)
     * 
     * @return bool PKCE support status
     */
    public function supports_pkce()
    {
        return $this->use_pkce;
    }
    
    /**
     * Check if tokens are encrypted (required by contract)
     * 
     * @return bool Token encryption status
     */
    public function are_tokens_encrypted()
    {
        return $this->token_manager->are_tokens_encrypted();
    }
}