<?php
/**
 * Cloudflare Worker Deployer
 * 
 * Orchestrates the deployment of Cloudflare Workers and R2 buckets.
 * Handles pre-flight checks, deployment steps, and rollback.
 * 
 * @package suspended-cloud/staticq
 * @since 2.1.0
 */

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

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

class SQMedia_Cloudflare_Deployer {
    
    /** @var SQMedia_Cloudflare_API */
    private SQMedia_Cloudflare_API $api;
    
    /** @var array Deployment state tracking */
    private array $deployment_state = [
        'bucket_created' => false,
        'bucket_name' => '',
        'worker_created' => false,
        'worker_name' => '',
        'route_created' => false,
        'route_id' => '',
        'zone_id' => '',
    ];
    
    /**
     * Constructor
     * 
     * @param SQMedia_Cloudflare_API|null $api API client instance
     */
    public function __construct(?SQMedia_Cloudflare_API $api = null) {
        $this->api = $api ?? new SQMedia_Cloudflare_API();
    }
    
    /**
     * Run pre-flight checks before deployment
     * 
     * @param array $config Deployment configuration
     * @return array{success: bool, checks: array, errors: array}
     */
    public function preflight_checks(array $config): array {
        $checks = [];
        $errors = [];
        
        // 1. Verify token
        $token_result = $this->api->verify_token();
        if (is_wp_error($token_result)) {
            $checks['token'] = ['status' => 'error', 'message' => $token_result->get_error_message()];
            $errors[] = 'token';
        } else {
            $checks['token'] = ['status' => 'ok', 'message' => __('Token is valid', 'staticq-media')];
        }
        
        // 2. Get account ID
        $account_id = $this->api->get_account_id();
        if (is_wp_error($account_id)) {
            $checks['account'] = ['status' => 'error', 'message' => $account_id->get_error_message()];
            $errors[] = 'account';
        } else {
            $checks['account'] = ['status' => 'ok', 'message' => __('Account accessible', 'staticq-media'), 'account_id' => $account_id];
        }
        
        // Stop here if we can't get account
        if (in_array('account', $errors)) {
            return ['success' => false, 'checks' => $checks, 'errors' => $errors];
        }
        
        // 3. Check zone exists
        if (!empty($config['zone_id'])) {
            $zone = $this->api->get_zone($config['zone_id']);
            if (is_wp_error($zone)) {
                $checks['zone'] = ['status' => 'error', 'message' => $zone->get_error_message()];
                $errors[] = 'zone';
            } else {
                /* translators: %s: zone/domain name */
                $checks['zone'] = ['status' => 'ok', 'message' => sprintf(__('Zone "%s" accessible', 'staticq-media'), $zone['result']['name'] ?? '')];
            }
        }
        
        // 4. Check worker name availability
        if (!empty($config['worker_name'])) {
            $worker_available = $this->api->is_worker_name_available($config['worker_name']);
            if (is_wp_error($worker_available)) {
                $checks['worker_name'] = ['status' => 'error', 'message' => $worker_available->get_error_message()];
                $errors[] = 'worker_name';
            } elseif ($worker_available) {
                $checks['worker_name'] = ['status' => 'ok', 'message' => __('Worker name is available', 'staticq-media')];
            } else {
                $checks['worker_name'] = ['status' => 'warning', 'message' => __('Worker name already exists (will be updated)', 'staticq-media')];
            }
        }
        
        // 5. Check bucket name availability (only if creating new)
        if (!empty($config['bucket_name']) && empty($config['use_existing_bucket'])) {
            $bucket_available = $this->api->is_bucket_name_available($config['bucket_name']);
            if (is_wp_error($bucket_available)) {
                $checks['bucket_name'] = ['status' => 'error', 'message' => $bucket_available->get_error_message()];
                $errors[] = 'bucket_name';
            } elseif ($bucket_available) {
                $checks['bucket_name'] = ['status' => 'ok', 'message' => __('Bucket name is available', 'staticq-media')];
            } else {
                $checks['bucket_name'] = ['status' => 'error', 'message' => __('Bucket name is already taken', 'staticq-media')];
                $errors[] = 'bucket_name';
            }
        } elseif (!empty($config['use_existing_bucket'])) {
            $checks['bucket_name'] = ['status' => 'ok', 'message' => __('Using existing bucket', 'staticq-media')];
        }
        
        // 6. Check route availability
        if (!empty($config['zone_id']) && !empty($config['route_pattern'])) {
            $route_check = $this->api->check_route_availability($config['zone_id'], $config['route_pattern']);
            if (is_wp_error($route_check)) {
                $checks['route'] = ['status' => 'error', 'message' => $route_check->get_error_message()];
                $errors[] = 'route';
            } elseif ($route_check['available']) {
                $checks['route'] = ['status' => 'ok', 'message' => __('Route pattern is available', 'staticq-media')];
            } else {
                $conflict = $route_check['conflict'];
                $checks['route'] = [
                    'status' => 'error',
                    'message' => sprintf(
                        /* translators: %s: worker script name */
                        __('Route already assigned to worker "%s"', 'staticq-media'),
                        $conflict['script'] ?? 'unknown'
                    ),
                    'conflict' => $conflict
                ];
                $errors[] = 'route';
            }
        }
        
        return [
            'success' => empty($errors),
            'checks' => $checks,
            'errors' => $errors
        ];
    }
    
    /**
     * Deploy Cloudflare Worker infrastructure
     * 
     * @param array $config Deployment configuration
     * @param callable|null $progress_callback Called with progress updates
     * @return array{success: bool, steps: array, error?: string}
     */
    public function deploy(array $config, ?callable $progress_callback = null): array {
        $steps = [];
        $this->deployment_state['zone_id'] = $config['zone_id'] ?? '';
        $provider = 'r2';

        $report_progress = function($step, $status, $message) use ($progress_callback, &$steps) {
            $steps[$step] = ['status' => $status, 'message' => $message];
            if ($progress_callback) {
                $progress_callback($step, $status, $message, $steps);
            }
        };

        try {
            // Step 1: Create or verify R2 bucket
            $report_progress('bucket', 'running', __('Setting up R2 bucket...', 'staticq-media'));

            if (empty($config['use_existing_bucket'])) {
                $bucket_result = $this->api->create_bucket($config['bucket_name']);
                if (is_wp_error($bucket_result)) {
                    $report_progress('bucket', 'error', $bucket_result->get_error_message());
                    return $this->deployment_failed('bucket', $bucket_result->get_error_message(), $steps);
                }
                $this->deployment_state['bucket_created'] = true;
                $this->deployment_state['bucket_name'] = $config['bucket_name'];
                /* translators: %s: bucket name */
                $report_progress('bucket', 'complete', sprintf(__('Created bucket "%s"', 'staticq-media'), $config['bucket_name']));
            } else {
                $this->deployment_state['bucket_name'] = $config['bucket_name'];
                /* translators: %s: bucket name */
                $report_progress('bucket', 'complete', sprintf(__('Using existing bucket "%s"', 'staticq-media'), $config['bucket_name']));
            }

            // Step 2: Generate and upload worker script
            $report_progress('worker', 'running', __('Uploading worker script...', 'staticq-media'));

            $worker_config = [
                'uploads_prefix' => $config['uploads_prefix'] ?? '/wp-content/uploads/',
                'bucket_prefix' => $config['bucket_prefix'] ?? 'wp-content/uploads/',
                'bucket_name' => $config['bucket_name'] ?? '',
                'cache_ttl' => $config['cache_ttl'] ?? 2592000,
                'enable_fallback' => $config['enable_fallback'] ?? true,
                'enable_variant_fallback' => $config['enable_variant_fallback'] ?? true,
                'variant_fallback_cdn_ttl' => $config['variant_fallback_cdn_ttl'] ?? 86400,
            ];

            $worker_script = SQMedia_Cloudflare_Worker::generate($provider, $worker_config);

            $bindings = SQMedia_Cloudflare_Worker::get_bindings($provider, ['bucket_name' => $config['bucket_name']]);
            
            $worker_result = $this->api->upload_worker($config['worker_name'], $worker_script, $bindings);
            if (is_wp_error($worker_result)) {
                $report_progress('worker', 'error', $worker_result->get_error_message());
                return $this->deployment_failed('worker', $worker_result->get_error_message(), $steps);
            }
            
            $this->deployment_state['worker_created'] = true;
            $this->deployment_state['worker_name'] = $config['worker_name'];
            /* translators: %s: worker script name */
            $report_progress('worker', 'complete', sprintf(__('Deployed worker "%s"', 'staticq-media'), $config['worker_name']));
            
            // Step 3: Create route
            $report_progress('route', 'running', __('Creating worker route...', 'staticq-media'));
            
            $route_result = $this->api->create_route($config['zone_id'], $config['route_pattern'], $config['worker_name']);
            if (is_wp_error($route_result)) {
                $report_progress('route', 'error', $route_result->get_error_message());
                return $this->deployment_failed('route', $route_result->get_error_message(), $steps);
            }
            
            $this->deployment_state['route_created'] = true;
            $this->deployment_state['route_id'] = $route_result['result']['id'] ?? '';
            /* translators: %s: route pattern URL */
            $report_progress('route', 'complete', sprintf(__('Created route "%s"', 'staticq-media'), $config['route_pattern']));
            
            // Step 4: Save configuration
            $report_progress('save', 'running', __('Saving configuration...', 'staticq-media'));
            
            $this->save_deployment_config($config);
            $report_progress('save', 'complete', __('Configuration saved', 'staticq-media'));
            
            // Step 5: Test deployment
            $report_progress('test', 'running', __('Testing deployment...', 'staticq-media'));
            
            $test_result = $this->test_deployment($config);
            if ($test_result['success']) {
                /* translators: %d: response time in milliseconds */
                $report_progress('test', 'complete', sprintf(__('Worker responding in %dms', 'staticq-media'), $test_result['response_time'] ?? 0));
            } else {
                $report_progress('test', 'warning', __('Deployment complete but test request failed. This may be normal if no files are in the bucket yet.', 'staticq-media'));
            }
            
            return [
                'success' => true,
                'steps' => $steps,
                'deployment' => [
                    'worker_name' => $config['worker_name'],
                    'bucket_name' => $config['bucket_name'],
                    'route_pattern' => $config['route_pattern'],
                    'route_id' => $this->deployment_state['route_id'],
                ]
            ];
            
        } catch (Exception $e) {
            return $this->deployment_failed('exception', $e->getMessage(), $steps);
        }
    }
    
    /**
     * Handle deployment failure and trigger rollback
     */
    private function deployment_failed(string $failed_step, string $error_message, array $steps): array {
        // Attempt rollback
        $rollback_result = $this->rollback();
        
        return [
            'success' => false,
            'failed_step' => $failed_step,
            'error' => $error_message,
            'steps' => $steps,
            'rollback' => $rollback_result
        ];
    }
    
    /**
     * Rollback deployment (remove created resources)
     * 
     * @return array{success: bool, actions: array}
     */
    public function rollback(): array {
        $actions = [];
        
        // Remove route first
        if ($this->deployment_state['route_created'] && !empty($this->deployment_state['route_id'])) {
            $result = $this->api->delete_route($this->deployment_state['zone_id'], $this->deployment_state['route_id']);
            $actions['route'] = is_wp_error($result) ? 'failed' : 'removed';
        }
        
        // Remove worker
        if ($this->deployment_state['worker_created'] && !empty($this->deployment_state['worker_name'])) {
            $result = $this->api->delete_worker($this->deployment_state['worker_name']);
            $actions['worker'] = is_wp_error($result) ? 'failed' : 'removed';
        }
        
        // Remove bucket (only if we created it and it's empty)
        if ($this->deployment_state['bucket_created'] && !empty($this->deployment_state['bucket_name'])) {
            $result = $this->api->delete_bucket($this->deployment_state['bucket_name']);
            $actions['bucket'] = is_wp_error($result) ? 'failed' : 'removed';
        }
        
        return [
            'success' => !in_array('failed', $actions),
            'actions' => $actions
        ];
    }
    
    /**
     * Undeploy/remove existing deployment
     * 
     * @param bool $delete_bucket Whether to delete the R2 bucket
     * @return array{success: bool, actions: array, errors: array}
     */
    public function undeploy(bool $delete_bucket = false): array {
        $actions = [];
        $errors = [];
        
        $settings = get_option('sqmedia_cf_deployment', []);
        
        // Remove route
        if (!empty($settings['route_id']) && !empty($settings['zone_id'])) {
            $result = $this->api->delete_route($settings['zone_id'], $settings['route_id']);
            if (is_wp_error($result)) {
                // "Not found" errors are OK - the resource is already gone
                if ($this->is_not_found_error($result)) {
                    $actions['route'] = 'already_removed';
                } else {
                    $actions['route'] = 'failed';
                    $errors[] = 'route: ' . $result->get_error_message();
                }
            } else {
                $actions['route'] = 'removed';
            }
        }
        
        // Remove worker
        if (!empty($settings['worker_name'])) {
            $result = $this->api->delete_worker($settings['worker_name']);
            if (is_wp_error($result)) {
                // "Not found" errors are OK - the resource is already gone
                if ($this->is_not_found_error($result)) {
                    $actions['worker'] = 'already_removed';
                } else {
                    $actions['worker'] = 'failed';
                    $errors[] = 'worker: ' . $result->get_error_message();
                }
            } else {
                $actions['worker'] = 'removed';
            }
        }
        
        // Optionally remove bucket
        if ($delete_bucket && !empty($settings['bucket_name'])) {
            $result = $this->api->delete_bucket($settings['bucket_name']);
            if (is_wp_error($result)) {
                // "Not found" errors are OK - the resource is already gone
                if ($this->is_not_found_error($result)) {
                    $actions['bucket'] = 'already_removed';
                } else {
                    $actions['bucket'] = 'failed';
                    $errors[] = 'bucket: ' . $result->get_error_message();
                }
            } else {
                $actions['bucket'] = 'removed';
            }
        }
        
        // Clear saved settings (always, regardless of API errors)
        delete_option('sqmedia_cf_deployment');
        delete_option('sqmedia_cf_account_id');
        
        return [
            'success' => empty($errors),
            'actions' => $actions,
            'errors' => $errors
        ];
    }
    
    /**
     * Check if error is a "not found" type error
     * 
     * @param WP_Error $error
     * @return bool
     */
    private function is_not_found_error(\WP_Error $error): bool {
        $message = strtolower($error->get_error_message());
        $code = $error->get_error_code();
        
        // Check for common "not found" indicators
        return (
            $code === 404 ||
            $code === 'not_found' ||
            strpos($message, 'not found') !== false ||
            strpos($message, 'does not exist') !== false ||
            strpos($message, 'could not find') !== false
        );
    }
    
    /**
     * Test deployed worker
     * 
     * @param array $config Deployment configuration
     * @return array{success: bool, response_time?: int, source?: string, error?: string}
     */
    public function test_deployment(array $config): array {
        $zone_name = $config['zone_name'] ?? '';
        $uploads_prefix = $config['uploads_prefix'] ?? '/wp-content/uploads/';
        
        if (empty($zone_name)) {
            // Try to get zone name from zone_id
            $zone = $this->api->get_zone($config['zone_id']);
            if (!is_wp_error($zone)) {
                $zone_name = $zone['result']['name'] ?? '';
            }
        }
        
        if (empty($zone_name)) {
            return ['success' => false, 'error' => 'Could not determine zone name'];
        }
        
        // Test URL - request a non-existent file to verify worker responds
        $test_url = 'https://' . $zone_name . $uploads_prefix . 'staticq-worker-test-' . time() . '.txt';
        
        $start_time = microtime(true);
        
        $response = wp_remote_get($test_url, [
            'timeout' => 10,
            'sslverify' => true
        ]);
        
        $response_time = round((microtime(true) - $start_time) * 1000);
        
        if (is_wp_error($response)) {
            return ['success' => false, 'error' => $response->get_error_message()];
        }
        
        $headers = wp_remote_retrieve_headers($response);
        $source = $headers['x-staticq-source'] ?? null;
        
        // Worker is working if we get the x-staticq-source header
        if ($source !== null) {
            return [
                'success' => true,
                'response_time' => $response_time,
                'source' => $source
            ];
        }
        
        // Check if we got a response at all (might be 404 from origin which is OK)
        $status = wp_remote_retrieve_response_code($response);
        if ($status === 404) {
            // Could be worker returning 404 or origin - check for our header
            return [
                'success' => $source !== null,
                'response_time' => $response_time,
                'source' => $source ?? 'unknown'
            ];
        }
        
        return ['success' => false, 'error' => 'Worker not responding with expected headers'];
    }
    
    /**
     * Save deployment configuration to WordPress options
     */
    private function save_deployment_config(array $config): void {
        $deployment_data = [
            'worker_name' => $config['worker_name'],
            'bucket_name' => $config['bucket_name'],
            'zone_id' => $config['zone_id'],
            'zone_name' => $config['zone_name'] ?? '',
            'route_pattern' => $config['route_pattern'],
            'route_id' => $this->deployment_state['route_id'],
            'storage_provider' => $config['storage_provider'] ?? 'r2',
            'enable_fallback' => $config['enable_fallback'] ?? true,
            'deployed_at' => current_time('mysql'),
            'deployed_by' => get_current_user_id(),
        ];
        
        update_option('sqmedia_cf_deployment', $deployment_data);
        update_option('sqmedia_cf_account_id', $this->api->get_account_id());
    }
    
    /**
     * Get current deployment status
     * 
     * @return array|null Deployment info or null if not deployed
     */
    public static function get_deployment_status(): ?array {
        $deployment = get_option('sqmedia_cf_deployment');
        
        if (empty($deployment) || empty($deployment['worker_name'])) {
            return null;
        }
        
        return $deployment;
    }
    
    /**
     * Check if worker is currently deployed
     */
    public static function is_deployed(): bool {
        return self::get_deployment_status() !== null;
    }
}