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

<?php

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

/**
 * Enhanced Moloni API Client Library
 * 
 * Handles all API communication with Moloni ERP system
 * Implements rate limiting, retry logic, and comprehensive error handling
 * 
 * @package     DeskMoloni
 * @author      Descomplicar®
 * @copyright   2025 Descomplicar
 * @version     3.0.0
 */
class MoloniApiClient
{
    private $CI;
    
    // API configuration
    private $api_base_url = 'https://api.moloni.pt/v1/';
    private $api_timeout = 30;
    private $connect_timeout = 10;
    private $max_retries = 3;
    private $retry_delay = 1; // seconds
    
    // Rate limiting configuration
    private $requests_per_minute = 60;
    private $requests_per_hour = 1000;
    private $request_count_minute = 0;
    private $request_count_hour = 0;
    private $minute_window_start = 0;
    private $hour_window_start = 0;
    
    // OAuth handler
    private $oauth;
    
    // Request logging
    private $log_requests = true;
    private $log_responses = true;
    private $log_errors = true;
    
    // Circuit breaker pattern
    private $circuit_breaker_threshold = 5;
    private $circuit_breaker_timeout = 300; // 5 minutes
    private $circuit_breaker_failures = 0;
    private $circuit_breaker_last_failure = 0;
    
    /**
     * Moloni API Client Constructor
     *
     * Initializes the Moloni API client with OAuth handler, configuration loading,
     * rate limiting setup, and circuit breaker pattern initialization.
     *
     * @since 3.0.0
     * @author Descomplicar®
     * @throws Exception If OAuth library cannot be loaded or configuration fails
     */
    public function __construct()
    {
        $this->CI = &get_instance();
        $this->CI->load->library('desk_moloni/moloni_oauth');
        
        $this->oauth = $this->CI->moloni_oauth;
        
        // Load configuration
        $this->load_configuration();
    }
    
    /**
     * Load API client configuration
     */
    private function load_configuration(): void
    {
        $this->api_timeout = (int)get_option('desk_moloni_api_timeout', 30);
        $this->connect_timeout = (int)get_option('desk_moloni_connect_timeout', 10);
        $this->max_retries = (int)get_option('desk_moloni_max_retries', 3);
        $this->requests_per_minute = (int)get_option('desk_moloni_requests_per_minute', 60);
        $this->requests_per_hour = (int)get_option('desk_moloni_requests_per_hour', 1000);
        $this->log_requests = (bool)get_option('desk_moloni_log_requests', true);
        $this->circuit_breaker_threshold = (int)get_option('desk_moloni_circuit_breaker_threshold', 5);
    }
    
    /**
     * Configure API client settings and operational parameters
     *
     * Updates API client configuration including timeouts, retry policies,
     * rate limiting parameters, and logging preferences with persistent storage.
     *
     * @param array $config Configuration options array with keys:
     *                     - timeout: API request timeout in seconds
     *                     - max_retries: Maximum retry attempts for failed requests
     *                     - rate_limit_per_minute: Requests per minute limit
     *                     - rate_limit_per_hour: Requests per hour limit
     *                     - log_requests: Enable/disable request logging
     * @return void Configuration is applied and persisted
     * @throws Exception When configuration validation fails or storage errors
     * @since 3.0.0
     * @author Descomplicar®
     */
    public function configure(array $config = []): void
    {
        if (isset($config['timeout'])) {
            $this->api_timeout = (int)$config['timeout'];
            update_option('desk_moloni_api_timeout', $this->api_timeout);
        }
        
        if (isset($config['max_retries'])) {
            $this->max_retries = (int)$config['max_retries'];
            update_option('desk_moloni_max_retries', $this->max_retries);
        }
        
        if (isset($config['rate_limit_per_minute'])) {
            $this->requests_per_minute = (int)$config['rate_limit_per_minute'];
            update_option('desk_moloni_requests_per_minute', $this->requests_per_minute);
        }
        
        if (isset($config['rate_limit_per_hour'])) {
            $this->requests_per_hour = (int)$config['rate_limit_per_hour'];
            update_option('desk_moloni_requests_per_hour', $this->requests_per_hour);
        }
        
        if (isset($config['log_requests'])) {
            $this->log_requests = (bool)$config['log_requests'];
            update_option('desk_moloni_log_requests', $this->log_requests);
        }
        
        log_activity('Desk-Moloni: API client configuration updated');
        
        return true;
    }
    
    // =====================================================
    // OAuth 2.0 Token Exchange
    // =====================================================
    
    /**
     * Exchange OAuth authorization code for access tokens
     *
     * Handles OAuth 2.0 authorization code exchange process to obtain
     * access and refresh tokens for authenticated API requests.
     *
     * @param string $code Authorization code received from OAuth callback
     * @param string $redirect_uri Redirect URI used in authorization request
     * @return array|bool Token response array with access_token, refresh_token, expires_in, or false on failure
     * @throws Exception When token exchange fails or OAuth handler errors
     * @since 3.0.0
     * @author Descomplicar®
     */
    public function exchange_token(string $code, string $redirect_uri): array|bool
    {
        return $this->oauth->handle_callback($code);
    }
    
    // =====================================================
    // Customer Management
    // =====================================================
    
    /**
     * Retrieve paginated list of customers from Moloni
     *
     * Fetches customers with pagination support and optional filtering parameters.
     * Includes comprehensive error handling and rate limiting compliance.
     *
     * @param int $company_id Moloni company identifier
     * @param array $options Query options array with keys:
     *                      - qty: Number of records to retrieve (default: 50, max: 100)
     *                      - offset: Starting record offset for pagination (default: 0)
     *                      - search: Search term for customer filtering (optional)
     * @return array|bool Customer list array with customer data, or false on failure
     * @throws Exception When API request fails, company_id is invalid, or rate limits exceeded
     * @since 3.0.0
     * @author Descomplicar®
     */
    public function list_customers(int $company_id, array $options = []): array|bool
    {
        $params = array_merge([
            'company_id' => $company_id,
            'qty' => $options['qty'] ?? 50,
            'offset' => $options['offset'] ?? 0
        ], $options);
        
        return $this->make_request('customers/getAll', $params);
    }
    
    /**
     * Get customer by ID
     * 
     * @param int $customer_id Customer ID
     * @param int $company_id Company ID
     * @return array Customer data
     */
    public function get_customer(int $customer_id, int $company_id): array|bool
    {
        return $this->make_request('customers/getOne', [
            'customer_id' => $customer_id,
            'company_id' => $company_id
        ]);
    }
    
    /**
     * Create new customer in Moloni with comprehensive validation
     *
     * Creates a new customer record in Moloni with required field validation,
     * automatic default value assignment, and error handling.
     *
     * @param array $customer_data Customer data array with required keys:
     *                            - company_id: Moloni company identifier (required)
     *                            - name: Customer full name (required)
     *                            - vat: Customer VAT number (required)
     *                            - country_id: Country identifier (default: 1 for Portugal)
     *                            - email: Customer email address (optional)
     *                            - address: Customer address (optional)
     *                            - city: Customer city (optional)
     *                            - zip_code: Customer postal code (optional)
     * @return array|bool Created customer data with assigned customer_id, or false on failure
     * @throws InvalidArgumentException When required fields are missing or invalid
     * @throws Exception When API request fails or validation errors occur
     * @since 3.0.0
     * @author Descomplicar®
     */
    public function create_customer(array $customer_data): array|bool
    {
        $required_fields = ['company_id', 'name', 'vat'];
        $this->validate_required_fields($customer_data, $required_fields);
        
        // Set defaults
        $customer_data = array_merge([
            'country_id' => 1 // Portugal default
        ], $customer_data);
        
        return $this->make_request('customers/insert', $customer_data);
    }
    
    /**
     * Update existing customer
     * 
     * @param int $customer_id Customer ID
     * @param array $customer_data Updated customer data
     * @return array Updated customer
     */
    public function update_customer(int $customer_id, array $customer_data): array|bool
    {
        $required_fields = ['customer_id', 'company_id'];
        $update_data = array_merge($customer_data, [
            'customer_id' => $customer_id
        ]);
        
        $this->validate_required_fields($update_data, $required_fields);
        
        return $this->make_request('customers/update', $update_data);
    }
    
    /**
     * Search customers
     * 
     * @param int $company_id Company ID
     * @param string $search Search term
     * @param array $options Additional options
     * @return array Customer search results
     */
    public function search_customers(int $company_id, string $search, array $options = []): array|bool
    {
        $params = array_merge([
            'company_id' => $company_id,
            'search' => $search,
            'qty' => $options['qty'] ?? 50,
            'offset' => $options['offset'] ?? 0
        ], $options);
        
        return $this->make_request('customers/getAll', $params);
    }
    
    // =====================================================
    // Product Management
    // =====================================================
    
    /**
     * List products
     * 
     * @param int $company_id Company ID
     * @param array $options Query options
     * @return array Product list
     */
    public function list_products($company_id, $options = [])
    {
        $params = array_merge([
            'company_id' => $company_id,
            'qty' => $options['qty'] ?? 100,
            'offset' => $options['offset'] ?? 0
        ], $options);
        
        return $this->make_request('products/getAll', $params);
    }
    
    /**
     * Get product by ID
     * 
     * @param int $product_id Product ID
     * @param int $company_id Company ID
     * @return array Product data
     */
    public function get_product($product_id, $company_id)
    {
        return $this->make_request('products/getOne', [
            'product_id' => $product_id,
            'company_id' => $company_id
        ]);
    }
    
    /**
     * Create new product
     * 
     * @param array $product_data Product data
     * @return array Created product
     */
    public function create_product($product_data)
    {
        $required_fields = ['company_id', 'name', 'price'];
        $this->validate_required_fields($product_data, $required_fields);
        
        // Set defaults
        $product_data = array_merge([
            'unit_id' => 1,
            'has_stock' => 0
        ], $product_data);
        
        return $this->make_request('products/insert', $product_data);
    }
    
    /**
     * Update existing product
     * 
     * @param int $product_id Product ID
     * @param array $product_data Updated product data
     * @return array Updated product
     */
    public function update_product($product_id, $product_data)
    {
        $required_fields = ['product_id', 'company_id'];
        $update_data = array_merge($product_data, [
            'product_id' => $product_id
        ]);
        
        $this->validate_required_fields($update_data, $required_fields);
        
        return $this->make_request('products/update', $update_data);
    }
    
    /**
     * Search products
     * 
     * @param int $company_id Company ID
     * @param string $search Search term
     * @param array $options Additional options
     * @return array Product search results
     */
    public function search_products($company_id, $search, $options = [])
    {
        $params = array_merge([
            'company_id' => $company_id,
            'search' => $search,
            'qty' => $options['qty'] ?? 100,
            'offset' => $options['offset'] ?? 0
        ], $options);
        
        return $this->make_request('products/getAll', $params);
    }
    
    // =====================================================
    // Invoice Management
    // =====================================================
    
    /**
     * Create new invoice in Moloni with comprehensive validation
     *
     * Creates a complete invoice in Moloni with customer, products, and financial data.
     * Includes validation for all required fields and product line items.
     *
     * @param array $invoice_data Invoice data array with required keys:
     *                           - company_id: Moloni company identifier (required)
     *                           - customer_id: Customer identifier in Moloni (required)
     *                           - date: Invoice date in Y-m-d format (required)
     *                           - products: Array of product line items (required)
     *                             Each product must have:
     *                             - product_id: Product identifier
     *                             - name: Product/service name
     *                             - qty: Quantity (decimal)
     *                             - price: Unit price (decimal)
     *                           - document_set_id: Document series ID (optional)
     *                           - payment_method_id: Payment method ID (optional)
     * @return array|bool Created invoice data with document_id and PDF link, or false on failure
     * @throws InvalidArgumentException When required fields are missing or products array is invalid
     * @throws Exception When API request fails or validation errors occur
     * @since 3.0.0
     * @author Descomplicar®
     */
    public function create_invoice($invoice_data)
    {
        $required_fields = ['company_id', 'customer_id', 'date', 'products'];
        $this->validate_required_fields($invoice_data, $required_fields);
        
        // Validate products
        if (empty($invoice_data['products']) || !is_array($invoice_data['products'])) {
            throw new InvalidArgumentException('Invoice must contain at least one product');
        }
        
        foreach ($invoice_data['products'] as $product) {
            $this->validate_required_fields($product, ['product_id', 'name', 'qty', 'price']);
        }
        
        return $this->make_request('invoices/insert', $invoice_data);
    }
    
    /**
     * List invoices with pagination
     * 
     * @param int $company_id Company ID
     * @param array $options Query options (qty, offset, search)
     * @return array Invoice list
     */
    public function list_invoices($company_id, $options = [])
    {
        $params = array_merge([
            'company_id' => $company_id,
            'qty' => $options['qty'] ?? 50,
            'offset' => $options['offset'] ?? 0
        ], $options);
        
        return $this->make_request('invoices/getAll', $params);
    }
    
    /**
     * Get invoice by ID
     * 
     * @param int $invoice_id Invoice ID
     * @param int $company_id Company ID
     * @return array Invoice data
     */
    public function get_invoice($invoice_id, $company_id)
    {
        return $this->make_request('invoices/getOne', [
            'document_id' => $invoice_id,
            'company_id' => $company_id
        ]);
    }
    
    /**
     * Update existing invoice
     * 
     * @param int $invoice_id Invoice ID
     * @param array $invoice_data Updated invoice data
     * @return array Updated invoice
     */
    public function update_invoice($invoice_id, $invoice_data)
    {
        $required_fields = ['document_id', 'company_id'];
        $update_data = array_merge($invoice_data, [
            'document_id' => $invoice_id
        ]);
        
        $this->validate_required_fields($update_data, $required_fields);
        
        return $this->make_request('invoices/update', $update_data);
    }

    /**
     * Get invoice PDF
     * 
     * @param int $invoice_id Invoice ID
     * @param int $company_id Company ID
     * @return string PDF binary data or PDF URL
     */
    public function get_invoice_pdf($invoice_id, $company_id)
    {
        $response = $this->make_request('invoices/getPDFLink', [
            'document_id' => $invoice_id,
            'company_id' => $company_id
        ]);
        
        // If response contains PDF URL, download the PDF
        if (isset($response['url'])) {
            return $this->download_pdf($response['url']);
        }
        
        return $response;
    }
    
    // =====================================================
    // Estimate Management
    // =====================================================
    
    /**
     * List estimates
     * 
     * @param int $company_id Company ID
     * @param array $options Query options
     * @return array Estimate list
     */
    public function list_estimates($company_id, $options = [])
    {
        $params = array_merge([
            'company_id' => $company_id,
            'qty' => $options['qty'] ?? 50,
            'offset' => $options['offset'] ?? 0
        ], $options);
        
        return $this->make_request('estimates/getAll', $params);
    }
    
    /**
     * Get estimate by ID
     * 
     * @param int $estimate_id Estimate ID
     * @param int $company_id Company ID
     * @return array Estimate data
     */
    public function get_estimate($estimate_id, $company_id)
    {
        return $this->make_request('estimates/getOne', [
            'document_id' => $estimate_id,
            'company_id' => $company_id
        ]);
    }
    
    /**
     * Create estimate
     * 
     * @param array $estimate_data Estimate data
     * @return array Created estimate
     */
    public function create_estimate($estimate_data)
    {
        $required_fields = ['company_id', 'customer_id', 'date', 'products'];
        $this->validate_required_fields($estimate_data, $required_fields);
        
        // Validate products
        if (empty($estimate_data['products']) || !is_array($estimate_data['products'])) {
            throw new InvalidArgumentException('Estimate must contain at least one product');
        }
        
        foreach ($estimate_data['products'] as $product) {
            $this->validate_required_fields($product, ['product_id', 'name', 'qty', 'price']);
        }
        
        return $this->make_request('estimates/insert', $estimate_data);
    }
    
    // =====================================================
    // Company Management
    // =====================================================
    
    /**
     * Get all companies for authenticated user
     * 
     * @return array Company list
     */
    public function list_companies()
    {
        return $this->make_request('companies/getAll');
    }
    
    /**
     * Get company by ID
     * 
     * @param int $company_id Company ID
     * @return array Company data
     */
    public function get_company($company_id)
    {
        return $this->make_request('companies/getOne', [
            'company_id' => $company_id
        ]);
    }
    
    // =====================================================
    // Tax Management
    // =====================================================
    
    /**
     * List taxes for company
     * 
     * @param int $company_id Company ID
     * @return array Tax list
     */
    public function list_taxes($company_id)
    {
        return $this->make_request('taxes/getAll', [
            'company_id' => $company_id
        ]);
    }
    
    /**
     * Get tax by ID
     * 
     * @param int $tax_id Tax ID
     * @param int $company_id Company ID
     * @return array Tax data
     */
    public function get_tax($tax_id, $company_id)
    {
        return $this->make_request('taxes/getOne', [
            'tax_id' => $tax_id,
            'company_id' => $company_id
        ]);
    }
    
    // =====================================================
    // Document Sets Management
    // =====================================================
    
    /**
     * List document sets
     * 
     * @param int $company_id Company ID
     * @return array Document sets list
     */
    public function list_document_sets($company_id)
    {
        return $this->make_request('documentSets/getAll', [
            'company_id' => $company_id
        ]);
    }
    
    // =====================================================
    // Payment Methods Management
    // =====================================================
    
    /**
     * List payment methods
     * 
     * @param int $company_id Company ID
     * @return array Payment methods list
     */
    public function list_payment_methods($company_id)
    {
        return $this->make_request('paymentMethods/getAll', [
            'company_id' => $company_id
        ]);
    }
    
    // =====================================================
    // Product Categories Management
    // =====================================================
    
    /**
     * List product categories
     * 
     * @param int $company_id Company ID
     * @return array Categories list
     */
    public function list_product_categories($company_id)
    {
        return $this->make_request('productCategories/getAll', [
            'company_id' => $company_id
        ]);
    }
    
    /**
     * Create product category
     * 
     * @param array $category_data Category data
     * @return array Created category
     */
    public function create_product_category($category_data)
    {
        $required_fields = ['company_id', 'name'];
        $this->validate_required_fields($category_data, $required_fields);
        
        return $this->make_request('productCategories/insert', $category_data);
    }
    
    // =====================================================
    // Units Management
    // =====================================================
    
    /**
     * List measurement units
     * 
     * @param int $company_id Company ID
     * @return array Units list
     */
    public function list_units($company_id)
    {
        return $this->make_request('measurementUnits/getAll', [
            'company_id' => $company_id
        ]);
    }
    
    // =====================================================
    // Countries and Geographical Data
    // =====================================================
    
    /**
     * List countries
     * 
     * @return array Countries list
     */
    public function list_countries()
    {
        return $this->make_request('countries/getAll');
    }
    
    /**
     * List delivery methods
     * 
     * @param int $company_id Company ID
     * @return array Delivery methods list
     */
    public function list_delivery_methods($company_id)
    {
        return $this->make_request('deliveryMethods/getAll', [
            'company_id' => $company_id
        ]);
    }
    
    // =====================================================
    // Core Request Handling
    // =====================================================
    
    /**
     * Execute HTTP request to Moloni API with comprehensive error handling
     *
     * Core API request method with circuit breaker pattern, rate limiting,
     * OAuth token management, retry logic with exponential backoff, and detailed logging.
     *
     * @param string $endpoint API endpoint path (without base URL)
     * @param array $params Request parameters for POST body or GET query string
     * @param string $method HTTP method - POST, GET, PUT, DELETE (default: POST)
     * @return array|bool API response data array, or false on failure
     * @throws Exception When circuit breaker is open, rate limits exceeded, OAuth not connected,
     *                  request fails after all retry attempts, or API returns errors
     * @since 3.0.0
     * @author Descomplicar®
     */
    public function make_request(string $endpoint, array $params = [], string $method = 'POST'): array|bool
    {
        // Check circuit breaker
        if ($this->is_circuit_open()) {
            throw new Exception('Circuit breaker is open - too many recent failures');
        }
        
        // Apply rate limiting
        $this->enforce_rate_limits();
        
        // Ensure OAuth connection
        if (!$this->oauth->is_connected()) {
            throw new Exception('OAuth not connected');
        }
        
        $url = $this->api_base_url . $endpoint;
        $access_token = $this->oauth->get_access_token();
        
        $last_exception = null;
        
        for ($attempt = 1; $attempt <= $this->max_retries; $attempt++) {
            try {
                $response = $this->execute_request($url, $params, $access_token, $method);
                
                // Reset circuit breaker on success
                $this->circuit_breaker_failures = 0;
                
                // Log successful request
                if ($this->log_requests) {
                    $this->log_api_call($endpoint, $params, $response, null, $attempt);
                }
                
                return $response;
                
            } catch (Exception $e) {
                $last_exception = $e;
                
                // Handle authentication errors
                if ($this->is_auth_error($e) && $attempt === 1) {
                    if ($this->oauth->refresh_access_token()) {
                        $access_token = $this->oauth->get_access_token();
                        continue; // Retry with new token
                    }
                }
                
                // Check if it's a rate limit error
                if ($this->is_rate_limit_error($e)) {
                    $wait_time = $this->calculate_rate_limit_wait($e);
                    if ($wait_time > 0 && $wait_time <= 60) {
                        sleep($wait_time);
                        continue; // Retry after waiting
                    }
                }
                
                // Don't retry on client errors (4xx except 401, 429)
                if ($this->is_client_error($e) && !$this->is_auth_error($e) && !$this->is_rate_limit_error($e)) {
                    break;
                }
                
                // Wait before retry (exponential backoff)
                if ($attempt < $this->max_retries) {
                    $wait_time = $this->retry_delay * pow(2, $attempt - 1);
                    sleep($wait_time);
                }
            }
        }
        
        // Update circuit breaker
        $this->circuit_breaker_failures++;
        $this->circuit_breaker_last_failure = time();
        
        // Log failure
        if ($this->log_errors && $last_exception) {
            $this->log_api_call($endpoint, $params, null, $last_exception->getMessage(), $this->max_retries);
        }
        
        throw new Exception("API request failed after {$this->max_retries} attempts: " . $last_exception->getMessage());
    }
    
    /**
     * Execute HTTP request
     * 
     * @param string $url Request URL
     * @param array $params Request parameters
     * @param string $access_token OAuth access token
     * @param string $method HTTP method
     * @return array Response data
     * @throws Exception On request failure
     */
    private function execute_request($url, $params, $access_token, $method = 'POST')
    {
        $ch = curl_init();
        
        $headers = [
            'Authorization: Bearer ' . $access_token,
            'Accept: application/json',
            'User-Agent: Desk-Moloni/3.0',
            'Cache-Control: no-cache'
        ];
        
        if ($method === 'POST') {
            $headers[] = 'Content-Type: application/json';
            
            curl_setopt_array($ch, [
                CURLOPT_URL => $url,
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => json_encode($params),
            ]);
        } else {
            if (!empty($params)) {
                $url .= '?' . http_build_query($params);
            }
            curl_setopt($ch, CURLOPT_URL, $url);
        }
        
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => $this->api_timeout,
            CURLOPT_CONNECTTIMEOUT => $this->connect_timeout,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_MAXREDIRS => 0,
            CURLOPT_ENCODING => '', // Enable compression
            CURLOPT_USERAGENT => 'Desk-Moloni/3.0 (Perfex CRM Integration)'
        ]);
        
        $response = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curl_error = curl_error($ch);
        $info = curl_getinfo($ch);
        
        curl_close($ch);
        
        if ($curl_error) {
            throw new Exception("CURL Error: {$curl_error}");
        }
        
        $decoded = json_decode($response, true);
        
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new Exception('Invalid JSON response from API: ' . json_last_error_msg());
        }
        
        // Handle HTTP errors
        if ($http_code >= 400) {
            $error_msg = $this->extract_error_message($decoded, $http_code);
            throw new Exception("HTTP {$http_code}: {$error_msg}");
        }
        
        // Check for API-level errors
        if (isset($decoded['error'])) {
            $error_msg = $decoded['error']['message'] ?? $decoded['error'];
            throw new Exception("Moloni API Error: {$error_msg}");
        }
        
        return $decoded;
    }
    
    // =====================================================
    // Helper Methods
    // =====================================================
    
    /**
     * Validate required fields in data array
     * 
     * @param array $data Data to validate
     * @param array $required_fields Required field names
     * @throws InvalidArgumentException If required fields are missing
     */
    private function validate_required_fields($data, $required_fields)
    {
        $missing_fields = [];
        
        foreach ($required_fields as $field) {
            if (!isset($data[$field]) || $data[$field] === '' || $data[$field] === null) {
                $missing_fields[] = $field;
            }
        }
        
        if (!empty($missing_fields)) {
            throw new InvalidArgumentException('Missing required fields: ' . implode(', ', $missing_fields));
        }
    }
    
    /**
     * Check if error is authentication related
     * 
     * @param Exception $exception Exception to check
     * @return bool True if auth error
     */
    private function is_auth_error($exception)
    {
        $message = strtolower($exception->getMessage());
        
        return strpos($message, 'unauthorized') !== false ||
               strpos($message, 'invalid_token') !== false ||
               strpos($message, 'token_expired') !== false ||
               strpos($message, 'http 401') !== false;
    }
    
    /**
     * Check if error is rate limit related
     * 
     * @param Exception $exception Exception to check
     * @return bool True if rate limit error
     */
    private function is_rate_limit_error($exception)
    {
        $message = strtolower($exception->getMessage());
        
        return strpos($message, 'rate limit') !== false ||
               strpos($message, 'too many requests') !== false ||
               strpos($message, 'http 429') !== false;
    }
    
    /**
     * Check if error is client error (4xx)
     * 
     * @param Exception $exception Exception to check
     * @return bool True if client error
     */
    private function is_client_error($exception)
    {
        $message = $exception->getMessage();
        
        return preg_match('/HTTP 4\d{2}/', $message);
    }
    
    /**
     * Calculate wait time for rate limit error
     * 
     * @param Exception $exception Rate limit exception
     * @return int Wait time in seconds
     */
    private function calculate_rate_limit_wait($exception)
    {
        // Default wait time
        $default_wait = 60;
        
        // Try to extract wait time from error message
        $message = $exception->getMessage();
        if (preg_match('/retry after (\d+)/i', $message, $matches)) {
            return min((int)$matches[1], 300); // Max 5 minutes
        }
        
        return $default_wait;
    }
    
    /**
     * Extract error message from API response
     * 
     * @param array|null $response Decoded response
     * @param int $http_code HTTP status code
     * @return string Error message
     */
    private function extract_error_message($response, $http_code)
    {
        if (is_array($response)) {
            if (isset($response['error']['message'])) {
                return $response['error']['message'];
            }
            if (isset($response['error'])) {
                return is_string($response['error']) ? $response['error'] : 'API Error';
            }
            if (isset($response['message'])) {
                return $response['message'];
            }
        }
        
        // Default HTTP error messages
        $http_messages = [
            400 => 'Bad Request',
            401 => 'Unauthorized',
            403 => 'Forbidden',
            404 => 'Not Found',
            429 => 'Too Many Requests',
            500 => 'Internal Server Error',
            502 => 'Bad Gateway',
            503 => 'Service Unavailable',
            504 => 'Gateway Timeout'
        ];
        
        return $http_messages[$http_code] ?? "HTTP Error {$http_code}";
    }
    
    /**
     * Enforce API rate limits
     * 
     * @throws Exception If rate limit exceeded
     */
    private function enforce_rate_limits()
    {
        $current_time = time();
        
        // Check minute window
        if ($current_time - $this->minute_window_start >= 60) {
            $this->minute_window_start = $current_time;
            $this->request_count_minute = 0;
        }
        
        // Check hour window
        if ($current_time - $this->hour_window_start >= 3600) {
            $this->hour_window_start = $current_time;
            $this->request_count_hour = 0;
        }
        
        // Enforce limits
        if ($this->request_count_minute >= $this->requests_per_minute) {
            $wait_time = 60 - ($current_time - $this->minute_window_start);
            throw new Exception("Rate limit exceeded: {$this->requests_per_minute} requests per minute. Wait {$wait_time} seconds.");
        }
        
        if ($this->request_count_hour >= $this->requests_per_hour) {
            $wait_time = 3600 - ($current_time - $this->hour_window_start);
            throw new Exception("Rate limit exceeded: {$this->requests_per_hour} requests per hour. Wait {$wait_time} seconds.");
        }
        
        // Increment counters
        $this->request_count_minute++;
        $this->request_count_hour++;
    }
    
    /**
     * Check if circuit breaker is open
     * 
     * @return bool True if circuit is open
     */
    private function is_circuit_open()
    {
        if ($this->circuit_breaker_failures < $this->circuit_breaker_threshold) {
            return false;
        }
        
        // Check if timeout has passed
        if (time() - $this->circuit_breaker_last_failure >= $this->circuit_breaker_timeout) {
            // Reset circuit breaker
            $this->circuit_breaker_failures = 0;
            return false;
        }
        
        return true;
    }
    
    /**
     * Download PDF from URL
     * 
     * @param string $url PDF URL
     * @return string PDF binary data
     */
    private function download_pdf($url)
    {
        $ch = curl_init();
        
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 60,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 3,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_USERAGENT => 'Desk-Moloni/3.0 PDF Downloader'
        ]);
        
        $pdf_data = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        
        curl_close($ch);
        
        if ($error || $http_code >= 400) {
            throw new Exception("PDF download failed: {$error} (HTTP {$http_code})");
        }
        
        return $pdf_data;
    }
    
    /**
     * Log API call for debugging and monitoring
     * 
     * @param string $endpoint API endpoint
     * @param array $params Request parameters
     * @param array|null $response Response data
     * @param string|null $error Error message
     * @param int $attempt Attempt number
     */
    private function log_api_call($endpoint, $params, $response, $error, $attempt)
    {
        if (!$this->log_requests && !$error) {
            return;
        }
        
        $log_data = [
            'endpoint' => $endpoint,
            'params' => $this->log_requests ? json_encode($params) : null,
            'response' => ($response && $this->log_responses) ? json_encode($response) : null,
            'error' => $error,
            'attempt' => $attempt,
            'timestamp' => date('Y-m-d H:i:s'),
            'user_id' => $this->CI->session->userdata('staff_user_id') ?? 0
        ];
        
        // Save to database if model is available
        if (method_exists($this->CI, 'load')) {
            try {
                // Optional: if a dedicated API log model exists
                if (file_exists(APPPATH . 'modules/desk_moloni/models/Desk_moloni_api_log_model.php')) {
                    $this->CI->load->model('desk_moloni/desk_moloni_api_log_model');
                    $this->CI->desk_moloni_api_log_model->insert($log_data);
                } else {
                    // Fallback to activity log
                    $message = "API Call: {$endpoint}";
                    if ($error) {
                        $message .= " - Error: {$error}";
                    }
                    log_activity($message);
                }
            } catch (Exception $e) {
                // Fallback to activity log
                $message = "API Call: {$endpoint}";
                if ($error) {
                    $message .= " - Error: {$error}";
                }
                log_activity($message);
            }
        }
    }
    
    /**
     * Retrieve comprehensive API client status and operational statistics
     *
     * Provides detailed information about API client health, configuration,
     * rate limiting status, circuit breaker state, and OAuth connectivity.
     *
     * @return array Status information array with keys:
     *              - oauth_connected: Boolean OAuth connection status
     *              - rate_limits: Current rate limiting statistics and configuration
     *              - circuit_breaker: Circuit breaker status and failure counts
     *              - configuration: Current client configuration settings
     * @since 3.0.0
     * @author Descomplicar®
     */
    public function get_status()
    {
        return [
            'oauth_connected' => $this->oauth->is_connected(),
            'rate_limits' => [
                'per_minute' => $this->requests_per_minute,
                'per_hour' => $this->requests_per_hour,
                'current_minute' => $this->request_count_minute,
                'current_hour' => $this->request_count_hour
            ],
            'circuit_breaker' => [
                'threshold' => $this->circuit_breaker_threshold,
                'failures' => $this->circuit_breaker_failures,
                'is_open' => $this->is_circuit_open(),
                'last_failure' => $this->circuit_breaker_last_failure
            ],
            'configuration' => [
                'timeout' => $this->api_timeout,
                'max_retries' => $this->max_retries,
                'base_url' => $this->api_base_url
            ]
        ];
    }
    
    // =====================================================
    // Webhook and Event Handling
    // =====================================================
    
    /**
     * Process webhook payload from Moloni
     * 
     * @param array $payload Webhook payload
     * @param string $signature Webhook signature for verification
     * @return bool Processing success
     */
    public function process_webhook($payload, $signature = null)
    {
        try {
            // Verify webhook signature if provided
            if ($signature) {
                $this->verify_webhook_signature($payload, $signature);
            }
            
            // Validate payload structure
            if (!isset($payload['event']) || !isset($payload['data'])) {
                throw new Exception('Invalid webhook payload structure');
            }
            
            $event = $payload['event'];
            $data = $payload['data'];
            
            // Log webhook received
            if ($this->log_requests) {
                $this->log_api_call("webhook:{$event}", $payload, null, null, 1);
            }
            
            // Process based on event type
            switch ($event) {
                case 'customer.created':
                case 'customer.updated':
                case 'customer.deleted':
                    return $this->handle_customer_webhook($event, $data);
                    
                case 'product.created':
                case 'product.updated':
                case 'product.deleted':
                    return $this->handle_product_webhook($event, $data);
                    
                case 'invoice.created':
                case 'invoice.updated':
                case 'invoice.paid':
                    return $this->handle_invoice_webhook($event, $data);
                    
                default:
                    // Log unknown event type
                    log_activity("Desk-Moloni: Unknown webhook event type: {$event}");
                    return true; // Don't fail for unknown events
            }
            
        } catch (Exception $e) {
            log_activity('Desk-Moloni: Webhook processing failed - ' . $e->getMessage());
            return false;
        }
    }
    
    /**
     * Verify webhook signature
     * 
     * @param array $payload Webhook payload
     * @param string $signature Provided signature
     * @throws Exception If signature is invalid
     */
    private function verify_webhook_signature($payload, $signature)
    {
        $webhook_secret = get_option('desk_moloni_webhook_secret');
        
        if (empty($webhook_secret)) {
            throw new Exception('Webhook secret not configured');
        }
        
        $expected_signature = hash_hmac('sha256', json_encode($payload), $webhook_secret);
        
        if (!hash_equals($expected_signature, $signature)) {
            throw new Exception('Invalid webhook signature');
        }
    }
    
    /**
     * Handle customer webhook events
     * 
     * @param string $event Event type
     * @param array $data Event data
     * @return bool Processing success
     */
    private function handle_customer_webhook($event, $data)
    {
        // Load client sync service to handle the webhook
        if (method_exists($this->CI, 'load')) {
            $this->CI->load->library('desk_moloni/client_sync_service');
            
            switch ($event) {
                case 'customer.created':
                case 'customer.updated':
                    return $this->CI->client_sync_service->sync_from_moloni($data['customer_id'], $data['company_id']);
                    
                case 'customer.deleted':
                    return $this->CI->client_sync_service->handle_moloni_deletion($data['customer_id'], $data['company_id']);
                    
                default:
                    return true;
            }
        }
        
        return true;
    }
    
    /**
     * Handle product webhook events
     * 
     * @param string $event Event type
     * @param array $data Event data
     * @return bool Processing success
     */
    private function handle_product_webhook($event, $data)
    {
        // Load product sync service to handle the webhook
        if (method_exists($this->CI, 'load')) {
            $this->CI->load->library('desk_moloni/product_sync_service');
            
            switch ($event) {
                case 'product.created':
                case 'product.updated':
                    return $this->CI->product_sync_service->sync_from_moloni($data['product_id'], $data['company_id']);
                    
                case 'product.deleted':
                    return $this->CI->product_sync_service->handle_moloni_deletion($data['product_id'], $data['company_id']);
                    
                default:
                    return true;
            }
        }
        
        return true;
    }
    
    /**
     * Handle invoice webhook events
     * 
     * @param string $event Event type
     * @param array $data Event data
     * @return bool Processing success
     */
    private function handle_invoice_webhook($event, $data)
    {
        // Load invoice sync service to handle the webhook
        if (method_exists($this->CI, 'load')) {
            $this->CI->load->library('desk_moloni/invoice_sync_service');
            
            switch ($event) {
                case 'invoice.created':
                case 'invoice.updated':
                case 'invoice.paid':
                    return $this->CI->invoice_sync_service->sync_from_moloni($data['document_id'], $data['company_id']);
                    
                default:
                    return true;
            }
        }
        
        return true;
    }
    
    // =====================================================
    // Bulk Operations
    // =====================================================
    
    /**
     * Bulk create customers
     * 
     * @param array $customers Array of customer data
     * @return array Results with success/failure for each customer
     */
    public function bulk_create_customers($customers)
    {
        $results = [];
        
        foreach ($customers as $index => $customer_data) {
            try {
                $result = $this->create_customer($customer_data);
                $results[$index] = [
                    'success' => true,
                    'data' => $result,
                    'error' => null
                ];
            } catch (Exception $e) {
                $results[$index] = [
                    'success' => false,
                    'data' => null,
                    'error' => $e->getMessage()
                ];
            }
        }
        
        return $results;
    }
    
    /**
     * Bulk create products
     * 
     * @param array $products Array of product data
     * @return array Results with success/failure for each product
     */
    public function bulk_create_products($products)
    {
        $results = [];
        
        foreach ($products as $index => $product_data) {
            try {
                $result = $this->create_product($product_data);
                $results[$index] = [
                    'success' => true,
                    'data' => $result,
                    'error' => null
                ];
            } catch (Exception $e) {
                $results[$index] = [
                    'success' => false,
                    'data' => null,
                    'error' => $e->getMessage()
                ];
            }
        }
        
        return $results;
    }
    
    /**
     * Bulk update entities with error handling
     * 
     * @param string $entity_type Entity type (customers, products, invoices)
     * @param array $updates Array of update data with IDs
     * @return array Results with success/failure for each update
     */
    public function bulk_update($entity_type, $updates)
    {
        $results = [];
        
        foreach ($updates as $index => $update_data) {
            try {
                switch ($entity_type) {
                    case 'customers':
                        $result = $this->update_customer($update_data['customer_id'], $update_data);
                        break;
                        
                    case 'products':
                        $result = $this->update_product($update_data['product_id'], $update_data);
                        break;
                        
                    case 'invoices':
                        $result = $this->update_invoice($update_data['document_id'], $update_data);
                        break;
                        
                    default:
                        throw new Exception("Unsupported entity type: {$entity_type}");
                }
                
                $results[$index] = [
                    'success' => true,
                    'data' => $result,
                    'error' => null
                ];
                
            } catch (Exception $e) {
                $results[$index] = [
                    'success' => false,
                    'data' => null,
                    'error' => $e->getMessage()
                ];
            }
        }
        
        return $results;
    }
    
    /**
     * Perform comprehensive API client and service health check
     *
     * Executes a complete health assessment including OAuth connectivity,
     * API endpoint availability, circuit breaker status, and rate limiting health.
     * Used for monitoring and diagnostics.
     *
     * @return array Health check results array with keys:
     *              - overall_status: 'healthy' or 'unhealthy' based on all checks
     *              - checks: Detailed results for each health check component
     *                - oauth: OAuth connection status and token validity
     *                - api_connectivity: API endpoint accessibility test
     *                - circuit_breaker: Circuit breaker operational status
     *                - rate_limits: Rate limiting usage and threshold status
     *              - timestamp: Health check execution timestamp
     * @since 3.0.0
     * @author Descomplicar®
     */
    public function health_check()
    {
        $results = [
            'overall_status' => 'healthy',
            'checks' => [],
            'timestamp' => date('Y-m-d H:i:s')
        ];
        
        // OAuth connectivity check
        try {
            $oauth_status = $this->oauth->is_connected();
            $results['checks']['oauth'] = [
                'status' => $oauth_status ? 'pass' : 'fail',
                'message' => $oauth_status ? 'OAuth connected' : 'OAuth not connected'
            ];
            
            if (!$oauth_status) {
                $results['overall_status'] = 'unhealthy';
            }
        } catch (Exception $e) {
            $results['checks']['oauth'] = [
                'status' => 'fail',
                'message' => 'OAuth check failed: ' . $e->getMessage()
            ];
            $results['overall_status'] = 'unhealthy';
        }
        
        // API connectivity check
        try {
            $companies = $this->list_companies();
            $results['checks']['api_connectivity'] = [
                'status' => 'pass',
                'message' => 'API connectivity verified'
            ];
        } catch (Exception $e) {
            $results['checks']['api_connectivity'] = [
                'status' => 'fail',
                'message' => 'API connectivity failed: ' . $e->getMessage()
            ];
            $results['overall_status'] = 'unhealthy';
        }
        
        // Circuit breaker status
        $results['checks']['circuit_breaker'] = [
            'status' => $this->is_circuit_open() ? 'warning' : 'pass',
            'message' => $this->is_circuit_open() 
                ? 'Circuit breaker is open due to failures' 
                : 'Circuit breaker is closed',
            'failures' => $this->circuit_breaker_failures,
            'threshold' => $this->circuit_breaker_threshold
        ];
        
        // Rate limiting status
        $minute_usage_pct = ($this->request_count_minute / $this->requests_per_minute) * 100;
        $hour_usage_pct = ($this->request_count_hour / $this->requests_per_hour) * 100;
        
        $results['checks']['rate_limits'] = [
            'status' => ($minute_usage_pct > 90 || $hour_usage_pct > 90) ? 'warning' : 'pass',
            'message' => sprintf('Rate limit usage: %.1f%% (minute), %.1f%% (hour)', 
                $minute_usage_pct, $hour_usage_pct),
            'minute_usage' => $minute_usage_pct,
            'hour_usage' => $hour_usage_pct
        ];
        
        return $results;
    }
}