<?php
/**
 * Cloudflare API Client
 * 
 * Low-level API wrapper for Cloudflare Workers and R2 operations.
 * 
 * @package suspended-cloud/staticq
 * @since 2.1.0
 */

if (!defined('ABSPATH')) {
    exit;
}

if (class_exists('SQMedia_Cloudflare_API')) {
    return;
}

class SQMedia_Cloudflare_API {
    
    /** @var string Cloudflare API base URL */
    private const API_BASE = 'https://api.cloudflare.com/client/v4';
    
    /** @var string|null API token */
    private ?string $token = null;
    
    /** @var string|null Account ID (cached after first fetch) */
    private ?string $account_id = null;
    
    /** @var array Last API error */
    private array $last_error = [];
    
    /**
     * Constructor
     * 
     * @param string|null $token API token (optional, will try to get from settings)
     */
    public function __construct(?string $token = null) {
        $this->token = $token ?? $this->get_stored_token();
    }
    
    /**
     * Get stored token (wp-config.php constant takes priority)
     */
    private function get_stored_token(): ?string {
        // Priority 1: wp-config.php constant
        if (defined('SQMEDIA_CF_TOKEN') && !empty(SQMEDIA_CF_TOKEN)) {
            return SQMEDIA_CF_TOKEN;
        }
        
        // Priority 2: Encrypted database option
        $encrypted = get_option('sqmedia_cf_token_encrypted', '');
        if (!empty($encrypted)) {
            return $this->decrypt_token($encrypted);
        }
        
        return null;
    }
    
    /**
     * Encrypt token for database storage
     */
    public function encrypt_token(string $token): string {
        if (!function_exists('openssl_encrypt')) {
            // Fallback: base64 with salt (not ideal but better than plaintext)
            return base64_encode('sqmedia_' . $token . '_' . wp_salt('auth'));
        }
        
        $key = hash('sha256', wp_salt('auth'), true);
        $iv = openssl_random_pseudo_bytes(16);
        $encrypted = openssl_encrypt($token, 'AES-256-CBC', $key, 0, $iv);
        
        return base64_encode($iv . $encrypted);
    }
    
    /**
     * Decrypt token from database storage
     */
    public function decrypt_token(string $encrypted): ?string {
        if (!function_exists('openssl_decrypt')) {
            // Fallback decryption
            $decoded = base64_decode($encrypted);
            if (strpos($decoded, 'sqmedia_') === 0) {
                $salt = '_' . wp_salt('auth');
                return str_replace(['sqmedia_', $salt], '', $decoded);
            }
            return null;
        }
        
        $data = base64_decode($encrypted);
        if ($data === false || strlen($data) < 17) {
            return null;
        }
        
        $key = hash('sha256', wp_salt('auth'), true);
        $iv = substr($data, 0, 16);
        $encrypted_data = substr($data, 16);
        
        $decrypted = openssl_decrypt($encrypted_data, 'AES-256-CBC', $key, 0, $iv);
        
        return $decrypted !== false ? $decrypted : null;
    }
    
    /**
     * Set the API token
     */
    public function set_token(string $token): void {
        $this->token = $token;
        $this->account_id = null; // Reset cached account ID
    }
    
    /**
     * Check if token is configured
     */
    public function has_token(): bool {
        return !empty($this->token);
    }
    
    /**
     * Get last error
     */
    public function get_last_error(): array {
        return $this->last_error;
    }
    
    /**
     * Make API request
     * 
     * @param string $method HTTP method
     * @param string $endpoint API endpoint (relative to base URL)
     * @param array $body Request body (for POST/PUT)
     * @param array $headers Additional headers
     * @return array|WP_Error Response data or error
     */
    private function request(string $method, string $endpoint, array $body = [], array $headers = []) {
        if (empty($this->token)) {
            $this->last_error = ['code' => 'no_token', 'message' => 'API token not configured'];
            return new WP_Error('no_token', __('Cloudflare API token not configured.', 'staticq-media'));
        }
        
        $url = self::API_BASE . $endpoint;
        
        $default_headers = [
            'Authorization' => 'Bearer ' . $this->token,
            'Content-Type' => 'application/json',
        ];
        
        $args = [
            'method' => $method,
            'headers' => array_merge($default_headers, $headers),
            'timeout' => 30,
        ];
        
        // For local development: bypass SSL verification if needed
        // This helps with environments like Local by Flywheel that may not have proper CA certs
        if (defined('WP_LOCAL_DEV') && WP_LOCAL_DEV) {
            $args['sslverify'] = false;
        }
        
        // Also check for common local development indicators
        $site_url = get_site_url();
        if (strpos($site_url, '.local') !== false || 
            strpos($site_url, 'localhost') !== false || 
            strpos($site_url, '127.0.0.1') !== false) {
            $args['sslverify'] = false;
        }
        
        if (!empty($body) && in_array($method, ['POST', 'PUT', 'PATCH'])) {
            $args['body'] = json_encode($body);
        }
        
        $response = wp_remote_request($url, $args);
        
        if (is_wp_error($response)) {
            $this->last_error = [
                'code' => $response->get_error_code(),
                'message' => $response->get_error_message()
            ];
            return $response;
        }
        
        $status_code = wp_remote_retrieve_response_code($response);
        $body = json_decode(wp_remote_retrieve_body($response), true);
        
        if ($status_code >= 400 || (isset($body['success']) && $body['success'] === false)) {
            $error_message = $this->extract_error_message($body, $status_code);
            $error_code = $this->extract_error_code($body, $status_code);
            
            $this->last_error = [
                'code' => $error_code,
                'message' => $error_message,
                'status' => $status_code,
                'response' => $body
            ];
            
            return new WP_Error($error_code, $error_message);
        }
        
        $this->last_error = [];
        return $body;
    }
    
    /**
     * Upload worker script (special handling for multipart)
     */
    private function upload_worker_request(string $account_id, string $script_name, string $script_content, array $bindings = []) {
        if (empty($this->token)) {
            return new WP_Error('no_token', __('Cloudflare API token not configured.', 'staticq-media'));
        }
        
        $url = self::API_BASE . "/accounts/{$account_id}/workers/scripts/{$script_name}";
        
        // Build multipart form data
        $boundary = wp_generate_password(24, false);
        
        $body = '';
        
        // Add metadata part with bindings
        $metadata = [
            'main_module' => 'worker.js',
            'bindings' => $bindings
        ];
        
        $body .= "--{$boundary}\r\n";
        $body .= "Content-Disposition: form-data; name=\"metadata\"\r\n";
        $body .= "Content-Type: application/json\r\n\r\n";
        $body .= json_encode($metadata) . "\r\n";
        
        // Add worker script part
        $body .= "--{$boundary}\r\n";
        $body .= "Content-Disposition: form-data; name=\"worker.js\"; filename=\"worker.js\"\r\n";
        $body .= "Content-Type: application/javascript+module\r\n\r\n";
        $body .= $script_content . "\r\n";
        
        $body .= "--{$boundary}--\r\n";
        
        $response = wp_remote_request($url, [
            'method' => 'PUT',
            'headers' => [
                'Authorization' => 'Bearer ' . $this->token,
                'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
            ],
            'body' => $body,
            'timeout' => 60,
        ]);
        
        if (is_wp_error($response)) {
            $this->last_error = [
                'code' => $response->get_error_code(),
                'message' => $response->get_error_message()
            ];
            return $response;
        }
        
        $status_code = wp_remote_retrieve_response_code($response);
        $body = json_decode(wp_remote_retrieve_body($response), true);
        
        if ($status_code >= 400 || (isset($body['success']) && $body['success'] === false)) {
            $error_message = $this->extract_error_message($body, $status_code);
            $this->last_error = [
                'code' => 'upload_failed',
                'message' => $error_message,
                'status' => $status_code
            ];
            return new WP_Error('upload_failed', $error_message);
        }
        
        return $body;
    }
    
    /**
     * Extract error message from API response
     */
    private function extract_error_message(array $body, int $status_code): string {
        if (!empty($body['errors']) && is_array($body['errors'])) {
            $messages = array_map(function($e) {
                return $e['message'] ?? 'Unknown error';
            }, $body['errors']);
            return implode('; ', $messages);
        }
        
        if (!empty($body['error'])) {
            return $body['error'];
        }
        
        /* translators: %d: HTTP status code */
        return sprintf(__('API request failed with status %d', 'staticq-media'), $status_code);
    }
    
    /**
     * Extract error code from API response
     */
    private function extract_error_code($body, int $status_code): string {
        if (!empty($body['errors'][0]['code'])) {
            return (string) $body['errors'][0]['code'];
        }
        
        return 'http_' . $status_code;
    }
    
    // =========================================================================
    // TOKEN & ACCOUNT
    // =========================================================================
    
    /**
     * Verify API token is valid
     * 
     * @return array|WP_Error Token info or error
     */
    public function verify_token() {
        // Try the standard user endpoint first
        $result = $this->request('GET', '/user/tokens/verify');
        
        // If that fails, the token might be account-scoped
        // We'll verify by trying to list accounts instead
        if (is_wp_error($result)) {
            $accounts = $this->request('GET', '/accounts');
            if (!is_wp_error($accounts) && !empty($accounts['result'])) {
                // Token works - return a success structure
                return [
                    'success' => true,
                    'result' => [
                        'status' => 'active'
                    ]
                ];
            }
            return $result; // Return original error
        }
        
        return $result;
    }
    
    /**
     * Get accounts accessible with this token
     * 
     * @return array|WP_Error Accounts list or error
     */
    public function get_accounts() {
        return $this->request('GET', '/accounts');
    }
    
    /**
     * Get account ID (cached)
     * 
     * @return string|WP_Error Account ID or error
     */
    public function get_account_id() {
        if ($this->account_id) {
            return $this->account_id;
        }
        
        // Check stored setting first
        $stored = get_option('sqmedia_cf_account_id', '');
        if (!empty($stored)) {
            $this->account_id = $stored;
            return $this->account_id;
        }
        
        // Fetch from API
        $accounts = $this->get_accounts();
        if (is_wp_error($accounts)) {
            return $accounts;
        }
        
        if (empty($accounts['result'][0]['id'])) {
            return new WP_Error('no_account', __('No Cloudflare account found for this token.', 'staticq-media'));
        }
        
        $this->account_id = $accounts['result'][0]['id'];
        return $this->account_id;
    }
    
    // =========================================================================
    // ZONES
    // =========================================================================
    
    /**
     * Get zones (domains) for the account
     * 
     * @return array|WP_Error Zones list or error
     */
    public function get_zones() {
        return $this->request('GET', '/zones?per_page=50&status=active');
    }
    
    /**
     * Get a specific zone by ID
     * 
     * @param string $zone_id Zone ID
     * @return array|WP_Error Zone data or error
     */
    public function get_zone(string $zone_id) {
        return $this->request('GET', "/zones/{$zone_id}");
    }
    
    // =========================================================================
    // WORKERS
    // =========================================================================
    
    /**
     * List all workers for the account
     * 
     * @param string|null $account_id Account ID (uses cached if not provided)
     * @return array|WP_Error Workers list or error
     */
    public function list_workers(?string $account_id = null) {
        $account_id = $account_id ?? $this->get_account_id();
        if (is_wp_error($account_id)) {
            return $account_id;
        }
        
        return $this->request('GET', "/accounts/{$account_id}/workers/scripts");
    }
    
    /**
     * Check if a worker name is available
     * 
     * @param string $script_name Worker script name
     * @param string|null $account_id Account ID
     * @return bool|WP_Error True if available, false if taken, WP_Error on failure
     */
    public function is_worker_name_available(string $script_name, ?string $account_id = null) {
        $workers = $this->list_workers($account_id);
        if (is_wp_error($workers)) {
            return $workers;
        }
        
        $existing_names = array_column($workers['result'] ?? [], 'id');
        return !in_array($script_name, $existing_names, true);
    }
    
    /**
     * Upload/create a worker script
     * 
     * @param string $script_name Worker name
     * @param string $script_content JavaScript content
     * @param array $bindings Worker bindings (R2, KV, etc.)
     * @param string|null $account_id Account ID
     * @return array|WP_Error Result or error
     */
    public function upload_worker(string $script_name, string $script_content, array $bindings = [], ?string $account_id = null) {
        $account_id = $account_id ?? $this->get_account_id();
        if (is_wp_error($account_id)) {
            return $account_id;
        }
        
        return $this->upload_worker_request($account_id, $script_name, $script_content, $bindings);
    }
    
    /**
     * Delete a worker script
     * 
     * @param string $script_name Worker name
     * @param string|null $account_id Account ID
     * @return array|WP_Error Result or error
     */
    public function delete_worker(string $script_name, ?string $account_id = null) {
        $account_id = $account_id ?? $this->get_account_id();
        if (is_wp_error($account_id)) {
            return $account_id;
        }
        
        return $this->request('DELETE', "/accounts/{$account_id}/workers/scripts/{$script_name}");
    }
    
    // =========================================================================
    // WORKER ROUTES
    // =========================================================================
    
    /**
     * List routes for a zone
     * 
     * @param string $zone_id Zone ID
     * @return array|WP_Error Routes list or error
     */
    public function list_routes(string $zone_id) {
        return $this->request('GET', "/zones/{$zone_id}/workers/routes");
    }
    
    /**
     * Check if a route pattern is available
     * 
     * @param string $zone_id Zone ID
     * @param string $pattern Route pattern
     * @return array{available: bool, conflict?: array}|WP_Error Availability info or error
     */
    public function check_route_availability(string $zone_id, string $pattern) {
        $routes = $this->list_routes($zone_id);
        if (is_wp_error($routes)) {
            return $routes;
        }
        
        foreach ($routes['result'] ?? [] as $route) {
            if ($route['pattern'] === $pattern) {
                return [
                    'available' => false,
                    'conflict' => [
                        'route_id' => $route['id'],
                        'script' => $route['script'] ?? 'unknown',
                        'pattern' => $route['pattern']
                    ]
                ];
            }
        }
        
        return ['available' => true];
    }
    
    /**
     * Create a worker route
     * 
     * @param string $zone_id Zone ID
     * @param string $pattern Route pattern (e.g., "example.com/wp-content/uploads/*")
     * @param string $script_name Worker script name
     * @return array|WP_Error Result or error
     */
    public function create_route(string $zone_id, string $pattern, string $script_name) {
        return $this->request('POST', "/zones/{$zone_id}/workers/routes", [
            'pattern' => $pattern,
            'script' => $script_name
        ]);
    }
    
    /**
     * Delete a worker route
     * 
     * @param string $zone_id Zone ID
     * @param string $route_id Route ID
     * @return array|WP_Error Result or error
     */
    public function delete_route(string $zone_id, string $route_id) {
        return $this->request('DELETE', "/zones/{$zone_id}/workers/routes/{$route_id}");
    }
    
    // =========================================================================
    // R2 BUCKETS
    // =========================================================================
    
    /**
     * List R2 buckets
     * 
     * @param string|null $account_id Account ID
     * @return array|WP_Error Buckets list or error
     */
    public function list_buckets(?string $account_id = null) {
        $account_id = $account_id ?? $this->get_account_id();
        if (is_wp_error($account_id)) {
            return $account_id;
        }
        
        return $this->request('GET', "/accounts/{$account_id}/r2/buckets");
    }
    
    /**
     * Check if a bucket name is available
     * 
     * @param string $bucket_name Bucket name
     * @param string|null $account_id Account ID
     * @return bool|WP_Error True if available, false if taken, WP_Error on failure
     */
    public function is_bucket_name_available(string $bucket_name, ?string $account_id = null) {
        $buckets = $this->list_buckets($account_id);
        if (is_wp_error($buckets)) {
            return $buckets;
        }
        
        $existing_names = array_column($buckets['result']['buckets'] ?? [], 'name');
        return !in_array($bucket_name, $existing_names, true);
    }
    
    /**
     * Create an R2 bucket
     * 
     * @param string $bucket_name Bucket name
     * @param string|null $account_id Account ID
     * @return array|WP_Error Result or error
     */
    public function create_bucket(string $bucket_name, ?string $account_id = null) {
        $account_id = $account_id ?? $this->get_account_id();
        if (is_wp_error($account_id)) {
            return $account_id;
        }
        
        return $this->request('PUT', "/accounts/{$account_id}/r2/buckets/{$bucket_name}", [
            'name' => $bucket_name
        ]);
    }
    
    /**
     * Delete an R2 bucket (must be empty)
     * 
     * @param string $bucket_name Bucket name
     * @param string|null $account_id Account ID
     * @return array|WP_Error Result or error
     */
    public function delete_bucket(string $bucket_name, ?string $account_id = null) {
        $account_id = $account_id ?? $this->get_account_id();
        if (is_wp_error($account_id)) {
            return $account_id;
        }
        
        return $this->request('DELETE', "/accounts/{$account_id}/r2/buckets/{$bucket_name}");
    }
}