<?php
declare(strict_types=1);


/**
 * CWV Data REST Controller
 *
 * @package ProRank\Core\RestApi
 */

namespace ProRank\SEO\Core\RestApi;

use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;

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


/**
 * Handles Core Web Vitals data endpoints
 */


class CwvDataController extends BaseController {
    
    /**
     * Routes base
     *
     * @var string
     */
    protected string $route_base = 'analytics';
    
    /**
     * Register routes
     *
     * @return void
     */
    public function register_routes(): void {
        // Get CWV data
        register_rest_route($this->namespace, '/' . $this->route_base . '/cwv-data', [
            [
                'methods' => WP_REST_Server::CREATABLE,
                'callback' => [$this, 'get_cwv_data'],
                'permission_callback' => [$this, 'check_permission'],
                'args' => [
                    'start_date' => [
                        'required' => true,
                        'type' => 'string',
                        'format' => 'date',
                        'description' => __('Start date (YYYY-MM-DD)', 'prorank-seo'),
                    ],
                    'end_date' => [
                        'required' => true,
                        'type' => 'string',
                        'format' => 'date',
                        'description' => __('End date (YYYY-MM-DD)', 'prorank-seo'),
                    ],
                    'url' => [
                        'type' => 'string',
                        'default' => 'all',
                        'description' => __('URL to filter by', 'prorank-seo'),
                    ],
                    'device' => [
                        'type' => 'string',
                        'enum' => ['mobile', 'desktop'],
                        'default' => 'mobile',
                        'description' => __('Device type', 'prorank-seo'),
                    ],
                ],
            ],
        ]);
        
        // Get available URLs
        register_rest_route($this->namespace, '/' . $this->route_base . '/cwv-urls', [
            [
                'methods' => WP_REST_Server::READABLE,
                'callback' => [$this, 'get_cwv_urls'],
                'permission_callback' => [$this, 'check_permission'],
            ],
        ]);
        
        // Get real-time Core Web Vitals
        register_rest_route($this->namespace, '/' . $this->route_base . '/cwv-realtime', [
            [
                'methods' => WP_REST_Server::CREATABLE,
                'callback' => [$this, 'get_realtime_cwv'],
                'permission_callback' => [$this, 'check_permission'],
                'args' => [
                    'url' => [
                        'type' => 'string',
                        'default' => '',
                        'description' => __('URL to analyze', 'prorank-seo'),
                    ],
                    'strategy' => [
                        'type' => 'string',
                        'enum' => ['mobile', 'desktop'],
                        'default' => 'mobile',
                        'description' => __('Device type', 'prorank-seo'),
                    ],
                ],
            ],
        ]);
        
        // Get CrUX (Chrome UX Report) data
        register_rest_route($this->namespace, '/' . $this->route_base . '/crux-data', [
            [
                'methods' => WP_REST_Server::READABLE,
                'callback' => [$this, 'get_crux_data'],
                'permission_callback' => [$this, 'check_permission'],
                'args' => [
                    'url' => [
                        'type' => 'string',
                        'default' => '',
                        'description' => __('URL to analyze', 'prorank-seo'),
                    ],
                ],
            ],
        ]);

        // Backward-compatible routes used by older admin UI screens.
        register_rest_route($this->namespace, '/core-web-vitals', [
            [
                'methods' => WP_REST_Server::READABLE,
                'callback' => [$this, 'get_core_web_vitals_compat'],
                'permission_callback' => [$this, 'check_permission'],
            ],
        ]);

        register_rest_route($this->namespace, '/core-web-vitals/history', [
            [
                'methods' => WP_REST_Server::READABLE,
                'callback' => [$this, 'get_core_web_vitals_history_compat'],
                'permission_callback' => [$this, 'check_permission'],
                'args' => [
                    'days' => [
                        'type' => 'integer',
                        'default' => 30,
                    ],
                ],
            ],
        ]);

        register_rest_route($this->namespace, '/core-web-vitals/crux', [
            [
                'methods' => WP_REST_Server::READABLE,
                'callback' => [$this, 'get_core_web_vitals_crux_compat'],
                'permission_callback' => [$this, 'check_permission'],
            ],
        ]);

        register_rest_route($this->namespace, '/core-web-vitals/budget', [
            [
                'methods' => WP_REST_Server::READABLE,
                'callback' => [$this, 'get_core_web_vitals_budget'],
                'permission_callback' => [$this, 'check_permission'],
            ],
            [
                'methods' => WP_REST_Server::CREATABLE,
                'callback' => [$this, 'save_core_web_vitals_budget'],
                'permission_callback' => [$this, 'check_permission'],
            ],
        ]);

        register_rest_route($this->namespace, '/core-web-vitals/save', [
            [
                'methods' => WP_REST_Server::CREATABLE,
                'callback' => [$this, 'save_core_web_vitals_measurement'],
                'permission_callback' => [$this, 'check_permission'],
            ],
        ]);

        register_rest_route($this->namespace, '/core-web-vitals/audit', [
            [
                'methods' => WP_REST_Server::CREATABLE,
                'callback' => [$this, 'run_core_web_vitals_audit'],
                'permission_callback' => [$this, 'check_permission'],
            ],
        ]);
    }
    
    /**
     * Get CWV data
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function get_cwv_data(WP_REST_Request $request) {
        global $wpdb;

        // Sanitize parameters
        $start_date = sanitize_text_field($request->get_param('start_date'));
        $end_date = sanitize_text_field($request->get_param('end_date'));
        $url = esc_url_raw($request->get_param('url'));
        $device = sanitize_key($request->get_param('device'));

        if (!function_exists('prorank_has_psi_api_key')) {
            require_once PRORANK_PLUGIN_DIR . 'includes/Config/pagespeed-config.php';
        }
        
        // Get PSI history data
        $table_name = $wpdb->prefix . 'prorank_psi_history';
        
        $where_clauses = [
            'timestamp BETWEEN %s AND %s',
            'strategy = %s',
        ];
        $where_values = [
            $start_date . ' 00:00:00',
            $end_date . ' 23:59:59',
            $device,
        ];
        
        if ($url !== 'all' && $url !== 'http://all') {
            $where_clauses[] = 'url = %s';
            $where_values[] = $url;
        }
        
        $where_sql = implode(' AND ', $where_clauses);
        $results = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT 
                    id,
                    url,
                    timestamp,
                    strategy,
                    lcp,
                    inp,
                    cls,
                    ttfb,
                    fcp,
                    speed_index,
                    tbt,
                    performance_score
                FROM %i
                WHERE {$where_sql}
                ORDER BY timestamp ASC",
                ...array_merge([$table_name], $where_values)
            )
        ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $results = array_values(array_filter($results, [$this, 'history_entry_is_public_with_metrics']));

        $diagnostics = [
            'psi_configured' => function_exists('prorank_has_psi_api_key') ? (bool) prorank_has_psi_api_key() : false,
            'gsc_connected' => false,
            'crux_available' => false,
            'has_history' => !empty($results),
            'history_count' => is_array($results) ? count($results) : 0,
            'has_current' => false,
            'current_source' => 'none',
            'messages' => [],
        ];

        // Get current (latest) values
        $current = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT 
                    url,
                    timestamp,
                    lcp,
                    inp,
                    cls,
                    fcp,
                    ttfb,
                    performance_score
                FROM %i
                WHERE {$where_sql}
                ORDER BY timestamp DESC
                LIMIT 1",
                ...array_merge([$table_name], $where_values)
            )
        ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        
        $crux_data = null;
        $normalized_crux_data = null;

        // 2025 Enhancement: Fetch real-time data if no historical current data
        $current_payload = is_object($current) ? (array) $current : (is_array($current) ? $current : []);
        if (!$this->has_metric_values($current_payload)) {
            require_once PRORANK_PLUGIN_DIR . 'includes/Core/Services/CoreWebVitalsService.php';
            $cwv_service = new \ProRank\SEO\Core\Services\CoreWebVitalsService();
            $realtime_url = ($url === 'all' || $url === 'http://all') ? get_site_url() : $url;
            $vitals = $cwv_service->get_vitals($realtime_url, $device);
            
            if (!is_wp_error($vitals) && !empty($vitals['lab_data'])) {
                $lab_data = $vitals['lab_data'] ?? [];
                $field_data = $vitals['field_data'] ?? [];
                
                // Transform to frontend format
                $current = [
                    'lcp' => isset($lab_data['lcp']['value']) ? $lab_data['lcp']['value'] : null,
                    'cls' => isset($lab_data['cls']['value']) ? $lab_data['cls']['value'] : null,
                    'fcp' => isset($lab_data['fcp']['value']) ? $lab_data['fcp']['value'] : null,
                    'inp' => isset($field_data['inp']['value']) ? $field_data['inp']['value'] : (isset($lab_data['tbt']['value']) ? $lab_data['tbt']['value'] : null),
                    'ttfb' => isset($field_data['ttfb']['value']) ? $field_data['ttfb']['value'] : null,
                    'si' => isset($lab_data['si']['value']) ? $lab_data['si']['value'] : null,
                    'score' => $vitals['score'] ?? null,
                    'url' => $realtime_url,
                    'timestamp' => current_time('mysql'),
                ];

                if (null === $normalized_crux_data) {
                    $normalized_crux_data = $this->normalize_crux_payload($field_data);
                }
                if (null === $crux_data && !empty($field_data)) {
                    $crux_data = $field_data;
                }
                $diagnostics['current_source'] = 'realtime';
                
                // Also store for history
                $cwv_service->store_vitals($realtime_url, $device, $vitals);
            } elseif (!is_wp_error($vitals)) {
                $current = [
                    'lcp' => null,
                    'inp' => null,
                    'cls' => null,
                    'fcp' => null,
                    'ttfb' => null,
                    'score' => null,
                    'url' => $realtime_url,
                    'timestamp' => current_time('mysql'),
                ];
                $diagnostics['current_source'] = 'realtime-empty';
            } elseif (is_wp_error($vitals)) {
                $diagnostics['messages'][] = sprintf(
                    /* translators: %s: PSI error message */
                    __('Realtime CWV fetch failed: %s', 'prorank-seo'),
                    $vitals->get_error_message()
                );
            }
        } else {
            $diagnostics['current_source'] = 'history';
        }

        // Generate insights after realtime fallback so we evaluate the actual current payload.
        $insights = $this->generate_insights(
            $results,
            $current,
            null !== $normalized_crux_data,
            (bool) $diagnostics['psi_configured']
        );

        $current_array = is_object($current) ? (array) $current : (is_array($current) ? $current : []);
        $diagnostics['has_current'] = !empty(array_filter([
            $current_array['lcp'] ?? null,
            $current_array['inp'] ?? null,
            $current_array['cls'] ?? null,
            $current_array['fcp'] ?? null,
            $current_array['ttfb'] ?? null,
        ], static function ($value) {
            return null !== $value;
        }));
        $diagnostics['crux_available'] = null !== $normalized_crux_data;

        if (!$diagnostics['psi_configured']) {
            $diagnostics['messages'][] = __('PageSpeed Insights API key is not configured. Add it in Analytics > Integrations > PageSpeed Insights.', 'prorank-seo');
        }
        if (!$diagnostics['has_history']) {
            $diagnostics['messages'][] = __('No historical CWV samples found yet for the selected range.', 'prorank-seo');
        }
        if (!$diagnostics['has_current']) {
            $diagnostics['messages'][] = __('Current CWV metrics are unavailable for the selected URL/device filter.', 'prorank-seo');
        }
        if ($diagnostics['gsc_connected'] && !$diagnostics['crux_available']) {
            $diagnostics['messages'][] = __('Chrome UX Report distributions are not available yet for this URL/device.', 'prorank-seo');
        }
        $diagnostics['messages'] = array_values(array_unique(array_filter($diagnostics['messages'])));
        
        return new WP_REST_Response([
            'history' => $results,
            'current' => $current,
            'crux_data' => $normalized_crux_data,
            'crux_raw' => $crux_data,
            'insights' => $insights,
            'diagnostics' => $diagnostics,
        ], 200);
    }
    
    /**
     * Get available URLs for filtering
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function get_cwv_urls(WP_REST_Request $request) {
        global $wpdb;

        $table_name = $wpdb->prefix . 'prorank_psi_history';
        
        $urls = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT DISTINCT url
                FROM %i
                ORDER BY url ASC
                LIMIT 100",
                $table_name
            )
        );
        
        return new WP_REST_Response([
            'urls' => $urls,
        ], 200);
    }
    
    /**
     * Get real-time Core Web Vitals
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function get_realtime_cwv(WP_REST_Request $request) {
        // Get parameters and sanitize
        $url = esc_url_raw($request->get_param('url'));
        $strategy = sanitize_key($request->get_param('strategy'));
        
        // Use Core Web Vitals service
        require_once PRORANK_PLUGIN_DIR . 'includes/Core/Services/CoreWebVitalsService.php';
        $cwv_service = new \ProRank\SEO\Core\Services\CoreWebVitalsService();
        
        // Fetch real-time data
        $vitals = $cwv_service->get_vitals($url, $strategy);
        
        if (is_wp_error($vitals)) {
            return $vitals;
        }
        
        // Store in database for history
        $cwv_service->store_vitals($url ?: home_url('/'), $strategy, $vitals);
        
        // Transform data to dashboard format in milliseconds where applicable.
        $lab_data = $vitals['lab_data'] ?? [];
        $field_data = $vitals['field_data'] ?? [];

        $transformed = [
            'lcp' => isset($lab_data['lcp']['value']) ? $lab_data['lcp']['value'] : null,
            'cls' => isset($lab_data['cls']['value']) ? $lab_data['cls']['value'] : null,
            'fcp' => isset($lab_data['fcp']['value']) ? $lab_data['fcp']['value'] : null,
            'inp' => isset($field_data['inp']['value']) ? $field_data['inp']['value'] : (isset($lab_data['tbt']['value']) ? $lab_data['tbt']['value'] : null),
            'ttfb' => isset($field_data['ttfb']['value']) ? $field_data['ttfb']['value'] : null,
            'si' => isset($lab_data['si']['value']) ? $lab_data['si']['value'] : null,
            'score' => $vitals['score'] ?? null,
            'crux_data' => $this->normalize_crux_payload($field_data),
            'raw_lab_data' => $lab_data,
        ];

        if (
            null === $transformed['lcp'] &&
            null === $transformed['inp'] &&
            null === $transformed['cls'] &&
            null === $transformed['fcp'] &&
            null === $transformed['ttfb']
        ) {
            $transformed['score'] = null;
        }

        return new WP_REST_Response($transformed, 200);
    }
    
    /**
     * Get CrUX (Chrome UX Report) data
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function get_crux_data(WP_REST_Request $request) {
        // Get parameters and sanitize
        $url = $request->get_param('url') ? esc_url_raw($request->get_param('url')) : home_url('/');
        
        // Use Core Web Vitals service to get field data (CrUX)
        require_once PRORANK_PLUGIN_DIR . 'includes/Core/Services/CoreWebVitalsService.php';
        $cwv_service = new \ProRank\SEO\Core\Services\CoreWebVitalsService();
        
        // Get data for both mobile and desktop
        $mobile_data = $cwv_service->get_vitals($url, 'mobile');
        $desktop_data = $cwv_service->get_vitals($url, 'desktop');
        
        $crux_data = [
            'url' => $url,
            'timestamp' => current_time('mysql'),
            'mobile' => null,
            'desktop' => null,
            'summary' => [],
        ];
        
        // Process mobile data
        if (!is_wp_error($mobile_data) && isset($mobile_data['field_data'])) {
            $crux_data['mobile'] = [
                'metrics' => $mobile_data['field_data'],
                'score' => $mobile_data['score'] ?? null,
            ];
        }
        
        // Process desktop data
        if (!is_wp_error($desktop_data) && isset($desktop_data['field_data'])) {
            $crux_data['desktop'] = [
                'metrics' => $desktop_data['field_data'],
                'score' => $desktop_data['score'] ?? null,
            ];
        }
        
        // Generate summary
        if ($crux_data['mobile'] || $crux_data['desktop']) {
            $crux_data['summary'] = $this->generate_crux_summary($crux_data);
        } else {
            $crux_data['summary'] = [
                'has_data' => false,
                'good_metrics' => 0,
                'needs_improvement' => 0,
                'poor_metrics' => 0,
                'insights' => [
                    __('No Chrome UX Report data available for this URL yet. CrUX data requires sufficient traffic over the past 28 days.', 'prorank-seo'),
                ],
            ];
            $crux_data['no_crux_data'] = true;
        }
        
        return new WP_REST_Response($crux_data, 200);
    }
    
    /**
     * Generate CrUX data summary
     *
     * @param array $data CrUX data
     * @return array Summary
     */
    private function generate_crux_summary(array $data): array {
        $summary = [
            'has_data' => false,
            'good_metrics' => 0,
            'needs_improvement' => 0,
            'poor_metrics' => 0,
            'insights' => [],
        ];
        
        // Check both mobile and desktop
        foreach (['mobile', 'desktop'] as $device) {
            if (!empty($data[$device]['metrics'])) {
                $summary['has_data'] = true;
                $metrics = $data[$device]['metrics'];
                
                // Check LCP
                if (isset($metrics['lcp']['value'])) {
                    $lcp = $metrics['lcp']['value'];
                    if ($lcp <= 2500) {
                        $summary['good_metrics']++;
                    } elseif ($lcp <= 4000) {
                        $summary['needs_improvement']++;
                    } else {
                        $summary['poor_metrics']++;
                    }
                }
                
                // Check INP
                if (isset($metrics['inp']['value'])) {
                    $inp = $metrics['inp']['value'];
                    if ($inp <= 200) {
                        $summary['good_metrics']++;
                    } elseif ($inp <= 500) {
                        $summary['needs_improvement']++;
                    } else {
                        $summary['poor_metrics']++;
                    }
                }
                
                // Check CLS
                if (isset($metrics['cls']['value'])) {
                    $cls = $metrics['cls']['value'] / 100; // Convert to decimal
                    if ($cls <= 0.1) {
                        $summary['good_metrics']++;
                    } elseif ($cls <= 0.25) {
                        $summary['needs_improvement']++;
                    } else {
                        $summary['poor_metrics']++;
                    }
                }
            }
        }
        
        // Generate insights
        if ($summary['poor_metrics'] > 0) {
            $summary['insights'][] = __('Your site has Core Web Vitals in the poor range that need immediate attention.', 'prorank-seo');
        }
        if ($summary['needs_improvement'] > 0) {
            $summary['insights'][] = __('Some metrics need improvement to pass Core Web Vitals assessment.', 'prorank-seo');
        }
        if ($summary['good_metrics'] >= 3) {
            $summary['insights'][] = __('Great! Your Core Web Vitals are passing for real users.', 'prorank-seo');
        }
        
        return $summary;
    }
    
    /**
     * Generate insights based on CWV data
     *
     * @param array $history Historical data
     * @param object|null $current Current values
     * @param bool        $has_crux Whether field-distribution data is available
     * @param bool        $psi_configured Whether a PSI API key is configured
     * @return array Insights
     */
    private function generate_insights(array $history, $current, bool $has_crux = false, bool $psi_configured = true): array {
        $insights = [];
        
        if (!$current) {
            $insights[] = __('No data available for the selected period.', 'prorank-seo');
            return $insights;
        }

        if (is_object($current)) {
            $current = (array) $current;
        }
        if (!is_array($current)) {
            $insights[] = __('No data available for the selected period.', 'prorank-seo');
            return $insights;
        }

        $has_current_metrics = $this->has_metric_values($current);
        $has_history_metrics = !empty(array_filter($history, [$this, 'history_entry_has_metrics']));

        if (!$has_current_metrics && !$has_history_metrics && !$has_crux) {
            $insights[] = $psi_configured
                ? __('Core Web Vitals recommendations will appear once measurement data is available for this site.', 'prorank-seo')
                : __('Core Web Vitals recommendations will appear after you configure a PageSpeed Insights API key and collect measurement data.', 'prorank-seo');
            return $insights;
        }

        $current_lcp = isset($current['lcp']) ? (float) $current['lcp'] : null;
        $current_inp = isset($current['inp']) ? (float) $current['inp'] : null;
        $current_cls = isset($current['cls']) ? (float) $current['cls'] : null;
        
        // LCP insights
        if ($current_lcp !== null && $current_lcp > 4.0) {
            $insights[] = __('LCP is in the poor range. Consider optimizing large images, fonts, and render-blocking resources.', 'prorank-seo');
        } elseif ($current_lcp !== null && $current_lcp > 2.5) {
            $insights[] = __('LCP needs improvement. Look into lazy loading images and optimizing server response times.', 'prorank-seo');
        }
        
        // INP insights
        if ($current_inp !== null && $current_inp > 500) {
            $insights[] = __('INP is poor. Reduce JavaScript execution time and optimize event handlers.', 'prorank-seo');
        } elseif ($current_inp !== null && $current_inp > 200) {
            $insights[] = __('INP could be better. Consider splitting long tasks and using web workers.', 'prorank-seo');
        }
        
        // CLS insights
        if ($current_cls !== null && $current_cls > 0.25) {
            $insights[] = __('CLS is in the poor range. Add size attributes to images and avoid inserting content above existing content.', 'prorank-seo');
        } elseif ($current_cls !== null && $current_cls > 0.1) {
            $insights[] = __('CLS needs work. Reserve space for dynamic content and avoid non-composited animations.', 'prorank-seo');
        }
        
        // Trend analysis
        if (count($history) > 7) {
            $recent = array_slice($history, -7);
            $older = array_slice($history, 0, 7);
            $recent_values = array_values(array_filter(array_map(static function ($row) {
                $payload = is_object($row) ? (array) $row : (is_array($row) ? $row : []);
                return isset($payload['lcp']) && is_numeric($payload['lcp']) ? (float) $payload['lcp'] : null;
            }, $recent), static function ($value) {
                return null !== $value;
            }));
            $older_values = array_values(array_filter(array_map(static function ($row) {
                $payload = is_object($row) ? (array) $row : (is_array($row) ? $row : []);
                return isset($payload['lcp']) && is_numeric($payload['lcp']) ? (float) $payload['lcp'] : null;
            }, $older), static function ($value) {
                return null !== $value;
            }));

            if (!empty($recent_values) && !empty($older_values)) {
                $recent_avg_lcp = array_sum($recent_values) / count($recent_values);
                $older_avg_lcp = array_sum($older_values) / count($older_values);

                if ($recent_avg_lcp > $older_avg_lcp * 1.1) {
                    $insights[] = __('LCP has been trending worse recently. Investigate recent changes.', 'prorank-seo');
                } elseif ($recent_avg_lcp < $older_avg_lcp * 0.9) {
                    $insights[] = __('Great! LCP has been improving over time.', 'prorank-seo');
                }
            }
        }
        
        if (empty($insights)) {
            $insights[] = __('Your Core Web Vitals are looking good! Keep monitoring for changes.', 'prorank-seo');
        }
        
        return $insights;
    }

    /**
     * Normalize CrUX payloads to the dashboard format:
     * metric => { good: <0-100>, needs_improvement: <0-100>, poor: <0-100> }.
     *
     * Supports:
     * - CrUX API response payloads (record.metrics.*.histogram)
     * - PSI loadingExperience-derived payloads (metric.distributions/category)
     *
     * @param mixed $payload Raw CrUX payload
     * @return array|null
     */
    private function normalize_crux_payload($payload): ?array {
        if (!is_array($payload) || empty($payload)) {
            return null;
        }

        $metrics_source = $payload;
        if (isset($payload['record']['metrics']) && is_array($payload['record']['metrics'])) {
            $metrics_source = $payload['record']['metrics'];
        }

        $normalized = [
            'lcp' => $this->normalize_metric_distribution($metrics_source, ['lcp', 'largest_contentful_paint', 'LARGEST_CONTENTFUL_PAINT_MS']),
            'inp' => $this->normalize_metric_distribution($metrics_source, ['inp', 'interaction_to_next_paint', 'INTERACTION_TO_NEXT_PAINT']),
            'cls' => $this->normalize_metric_distribution($metrics_source, ['cls', 'cumulative_layout_shift', 'CUMULATIVE_LAYOUT_SHIFT_SCORE']),
        ];

        $has_data = false;
        foreach ($normalized as $metric_distribution) {
            if (is_array($metric_distribution)) {
                $has_data = true;
                break;
            }
        }

        return $has_data ? $normalized : null;
    }

    /**
     * Normalize a metric distribution from supported CrUX payloads.
     *
     * @param array $source Metrics source payload
     * @param array $candidate_keys Candidate keys to locate metric data
     * @return array|null
     */
    private function normalize_metric_distribution(array $source, array $candidate_keys): ?array {
        $metric = null;
        foreach ($candidate_keys as $key) {
            if (isset($source[$key]) && is_array($source[$key])) {
                $metric = $source[$key];
                break;
            }
        }

        if (!$metric) {
            return null;
        }

        $good = 0;
        $needs_improvement = 0;
        $poor = 0;

        if (!empty($metric['histogram']) && is_array($metric['histogram'])) {
            [$good, $needs_improvement, $poor] = $this->extract_distribution_percentages($metric['histogram'], 'density');
        } elseif (!empty($metric['distributions']) && is_array($metric['distributions'])) {
            [$good, $needs_improvement, $poor] = $this->extract_distribution_percentages($metric['distributions'], 'proportion');
        } elseif (!empty($metric['category'])) {
            $category = strtoupper((string) $metric['category']);
            if ($category === 'FAST') {
                $good = 100;
            } elseif ($category === 'AVERAGE' || $category === 'NEEDS_IMPROVEMENT') {
                $needs_improvement = 100;
            } elseif ($category === 'SLOW' || $category === 'POOR') {
                $poor = 100;
            }
        }

        if (($good + $needs_improvement + $poor) <= 0) {
            return null;
        }

        return [
            'good' => $good,
            'needs_improvement' => $needs_improvement,
            'poor' => $poor,
        ];
    }

    /**
     * Determine whether a payload contains at least one usable metric.
     *
     * @param array $payload
     * @return bool
     */
    private function has_metric_values(array $payload): bool {
        foreach (['lcp', 'inp', 'cls', 'fcp', 'ttfb'] as $metric) {
            if (array_key_exists($metric, $payload) && null !== $payload[$metric] && '' !== $payload[$metric]) {
                return true;
            }
        }

        return false;
    }

    /**
     * Filter empty rows out of compat history payloads.
     *
     * @param mixed $entry
     * @return bool
     */
    private function history_entry_has_metrics($entry): bool {
        $payload = is_object($entry) ? (array) $entry : (is_array($entry) ? $entry : []);
        if (empty($payload)) {
            return false;
        }

        return $this->has_metric_values($payload);
    }

    /**
     * Filter callback for public measurement rows with usable metrics.
     *
     * @param mixed $entry
     * @return bool
     */
    private function history_entry_is_public_with_metrics($entry): bool {
        $payload = is_object($entry) ? (array) $entry : (is_array($entry) ? $entry : []);
        if (empty($payload) || !$this->history_entry_has_metrics($payload)) {
            return false;
        }

        return $this->is_public_measurement_url((string) ($payload['url'] ?? ''));
    }

    /**
     * Build the legacy CrUX payload expected by the admin UI.
     *
     * @param array $payload
     * @return array<string,array<string,float|null>>
     */
    private function build_compat_crux_metrics(array $payload): array {
        $metrics = [];

        foreach (['lcp', 'inp', 'cls'] as $metric) {
            $sources = [];
            foreach (['mobile', 'desktop'] as $device) {
                if (!empty($payload[$device]['metrics'][$metric]) && is_array($payload[$device]['metrics'][$metric])) {
                    $sources[] = $payload[$device]['metrics'][$metric];
                }
            }

            if (empty($sources)) {
                continue;
            }

            $good = [];
            $needs_improvement = [];
            $poor = [];
            $p75 = [];

            foreach ($sources as $source_metric) {
                $distribution = $this->build_compat_crux_distribution($source_metric);
                if (null !== $distribution) {
                    $good[] = $distribution['good'];
                    $needs_improvement[] = $distribution['needs_improvement'];
                    $poor[] = $distribution['poor'];
                }

                if (isset($source_metric['value']) && is_numeric($source_metric['value'])) {
                    $value = (float) $source_metric['value'];
                    if ($metric === 'cls' && $value > 1) {
                        $value = $value / 100;
                    }
                    $p75[] = $value;
                }
            }

            $metrics[$metric] = [
                'p75' => $this->average_nullable($p75),
                'good' => $this->average_nullable($good) ?? 0.0,
                'needs_improvement' => $this->average_nullable($needs_improvement) ?? 0.0,
                'poor' => $this->average_nullable($poor) ?? 0.0,
            ];
        }

        return $metrics;
    }

    /**
     * Normalize a single CrUX metric to 0..1 fractions.
     *
     * @param array $metric
     * @return array<string,float>|null
     */
    private function build_compat_crux_distribution(array $metric): ?array {
        $good = 0;
        $needs_improvement = 0;
        $poor = 0;

        if (!empty($metric['distributions']) && is_array($metric['distributions'])) {
            [$good, $needs_improvement, $poor] = $this->extract_distribution_percentages($metric['distributions'], 'proportion');
        } elseif (!empty($metric['histogram']) && is_array($metric['histogram'])) {
            [$good, $needs_improvement, $poor] = $this->extract_distribution_percentages($metric['histogram'], 'density');
        } elseif (!empty($metric['category'])) {
            $category = strtoupper((string) $metric['category']);
            if ($category === 'FAST') {
                $good = 100;
            } elseif ($category === 'AVERAGE' || $category === 'NEEDS_IMPROVEMENT') {
                $needs_improvement = 100;
            } elseif ($category === 'SLOW' || $category === 'POOR') {
                $poor = 100;
            }
        }

        if (($good + $needs_improvement + $poor) <= 0) {
            return null;
        }

        return [
            'good' => $good / 100,
            'needs_improvement' => $needs_improvement / 100,
            'poor' => $poor / 100,
        ];
    }

    /**
     * Describe the source device mix for CrUX compat payloads.
     *
     * @param array $payload
     * @return string
     */
    private function detect_crux_source_device(array $payload): string {
        $has_mobile = !empty($payload['mobile']['metrics']);
        $has_desktop = !empty($payload['desktop']['metrics']);

        if ($has_mobile && $has_desktop) {
            return 'combined';
        }
        if ($has_mobile) {
            return 'mobile';
        }
        if ($has_desktop) {
            return 'desktop';
        }

        return 'none';
    }

    /**
     * Average a numeric set, returning null for empty input.
     *
     * @param array<int,float|int> $values
     * @return float|null
     */
    private function average_nullable(array $values): ?float {
        $values = array_values(array_filter($values, static function ($value) {
            return is_numeric($value);
        }));

        if (empty($values)) {
            return null;
        }

        return array_sum($values) / count($values);
    }

    /**
     * Ignore admin/internal URLs when storing or surfacing site CWV samples.
     *
     * @param string $url
     * @return bool
     */
    private function is_public_measurement_url(string $url): bool {
        if ($url === '') {
            return false;
        }

        $path = (string) wp_parse_url($url, PHP_URL_PATH);
        $path = '/' . ltrim($path, '/');

        if (strpos($path, '/wp-admin') === 0 || strpos($path, '/wp-login.php') === 0 || strpos($path, '/wp-json') === 0) {
            return false;
        }

        return true;
    }

    /**
     * Extract good/needs-improvement/poor percentages from CrUX buckets.
     *
     * @param array $buckets Histogram/distribution buckets
     * @param string $weight_key Either "density" or "proportion"
     * @return array<int>
     */
    private function extract_distribution_percentages(array $buckets, string $weight_key): array {
        $values = [0.0, 0.0, 0.0];
        foreach (array_values($buckets) as $index => $bucket) {
            if ($index > 2 || !is_array($bucket)) {
                continue;
            }
            $value = isset($bucket[$weight_key]) ? (float) $bucket[$weight_key] : 0.0;
            $values[$index] = max(0.0, $value);
        }

        // Convert to percentages with stable rounding and a 100% total.
        $percentages = array_map(
            static function (float $value): int {
                return (int) round($value * 100);
            },
            $values
        );

        $sum = array_sum($percentages);
        if ($sum > 100) {
            $overflow = $sum - 100;
            for ($i = 2; $i >= 0 && $overflow > 0; $i--) {
                $reduce = min($overflow, $percentages[$i]);
                $percentages[$i] -= $reduce;
                $overflow -= $reduce;
            }
        } elseif ($sum > 0 && $sum < 100) {
            $percentages[0] += (100 - $sum);
        }

        return $percentages;
    }

    /**
     * Backward-compatible current CWV endpoint.
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response|\WP_Error
     */
    public function get_core_web_vitals_compat(WP_REST_Request $request) {
        $days = 30;
        $device = sanitize_key((string) ($request->get_param('device') ?: 'mobile'));
        $proxy_request = new \WP_REST_Request('POST', '/');
        $proxy_request->set_param('start_date', gmdate('Y-m-d', strtotime("-{$days} days")));
        $proxy_request->set_param('end_date', gmdate('Y-m-d'));
        $proxy_request->set_param('url', 'all');
        $proxy_request->set_param('device', $device);

        $response = $this->get_cwv_data($proxy_request);
        if (is_wp_error($response)) {
            return $response;
        }

        $payload = $response instanceof WP_REST_Response ? $response->get_data() : (array) $response;
        $current = $payload['current'] ?? [];
        if (is_object($current)) {
            $current = (array) $current;
        }

        $score = $current['score'] ?? ($current['performance_score'] ?? null);
        if (!$this->has_metric_values($current) && (null === $score || (float) $score <= 0)) {
            $score = null;
        }

        return new WP_REST_Response([
            'lcp' => $current['lcp'] ?? null,
            'inp' => $current['inp'] ?? null,
            'cls' => $current['cls'] ?? null,
            'fcp' => $current['fcp'] ?? null,
            'ttfb' => $current['ttfb'] ?? null,
            'score' => $score,
            'timestamp' => $current['timestamp'] ?? current_time('mysql'),
            'url' => $current['url'] ?? home_url('/'),
            'recommendations' => $payload['insights'] ?? [],
            'diagnostics' => $payload['diagnostics'] ?? [],
        ], 200);
    }

    /**
     * Backward-compatible CWV history endpoint.
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response|\WP_Error
     */
    public function get_core_web_vitals_history_compat(WP_REST_Request $request) {
        $days = max(1, min(365, (int) $request->get_param('days')));
        $proxy_request = new \WP_REST_Request('POST', '/');
        $proxy_request->set_param('start_date', gmdate('Y-m-d', strtotime("-{$days} days")));
        $proxy_request->set_param('end_date', gmdate('Y-m-d'));
        $proxy_request->set_param('url', 'all');
        $proxy_request->set_param('device', 'mobile');

        $response = $this->get_cwv_data($proxy_request);
        if (is_wp_error($response)) {
            return $response;
        }

        $payload = $response instanceof WP_REST_Response ? $response->get_data() : (array) $response;
        $history = $payload['history'] ?? [];
        if (!is_array($history)) {
            $history = [];
        }

        $history = array_values(array_filter($history, [$this, 'history_entry_is_public_with_metrics']));

        // Fallback to lightweight option-based history when DB table is empty.
        if (empty($history)) {
            $history = get_option('prorank_cwv_measurements', []);
            if (!is_array($history)) {
                $history = [];
            }
            $history = array_values(array_filter($history, [$this, 'history_entry_is_public_with_metrics']));
        }

        return new WP_REST_Response([
            'success' => true,
            'data' => array_values($history),
        ], 200);
    }

    /**
     * Backward-compatible CrUX endpoint.
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response|\WP_Error
     */
    public function get_core_web_vitals_crux_compat(WP_REST_Request $request) {
        $proxy_request = new \WP_REST_Request('GET', '/');
        $proxy_request->set_param('url', home_url('/'));
        $response = $this->get_crux_data($proxy_request);
        if (is_wp_error($response)) {
            return $response;
        }

        $payload = $response instanceof WP_REST_Response ? $response->get_data() : (array) $response;
        $metrics = $this->build_compat_crux_metrics($payload);

        return new WP_REST_Response([
            'available' => !empty($metrics),
            'metrics' => $metrics,
            'summary' => $payload['summary'] ?? [],
            'source_device' => $this->detect_crux_source_device($payload),
            'timestamp' => $payload['timestamp'] ?? current_time('mysql'),
            'url' => $payload['url'] ?? home_url('/'),
        ], 200);
    }

    /**
     * Get saved performance budget settings.
     *
     * @return WP_REST_Response
     */
    public function get_core_web_vitals_budget(): WP_REST_Response {
        $default_budgets = [
            [ 'metric' => 'lcp', 'threshold' => 2500, 'enabled' => true ],
            [ 'metric' => 'inp', 'threshold' => 200, 'enabled' => true ],
            [ 'metric' => 'cls', 'threshold' => 0.1, 'enabled' => true ],
            [ 'metric' => 'fcp', 'threshold' => 1800, 'enabled' => false ],
            [ 'metric' => 'ttfb', 'threshold' => 800, 'enabled' => false ],
        ];

        $budgets = get_option('prorank_cwv_budgets', $default_budgets);
        if (!is_array($budgets)) {
            $budgets = $default_budgets;
        }

        $alerts = get_option('prorank_cwv_budget_alerts', []);
        if (!is_array($alerts)) {
            $alerts = [];
        }

        return new WP_REST_Response([
            'success' => true,
            'budgets' => $budgets,
            'alerts' => $alerts,
        ], 200);
    }

    /**
     * Save performance budget settings.
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function save_core_web_vitals_budget(WP_REST_Request $request): WP_REST_Response {
        $budgets = $request->get_param('budgets');
        if (!is_array($budgets)) {
            $budgets = [];
        }

        $allowed_metrics = ['lcp', 'inp', 'cls', 'fcp', 'ttfb'];
        $sanitized = [];
        foreach ($budgets as $budget) {
            if (!is_array($budget)) {
                continue;
            }

            $metric = sanitize_key((string) ($budget['metric'] ?? ''));
            if (!in_array($metric, $allowed_metrics, true)) {
                continue;
            }

            $threshold = isset($budget['threshold']) ? (float) $budget['threshold'] : 0;
            if ($metric === 'cls') {
                $threshold = max(0, min(1, $threshold));
            } else {
                $threshold = max(0, $threshold);
            }

            $sanitized[] = [
                'metric' => $metric,
                'threshold' => $threshold,
                'enabled' => !empty($budget['enabled']),
            ];
        }

        update_option('prorank_cwv_budgets', $sanitized, false);

        return new WP_REST_Response([
            'success' => true,
            'message' => __('Performance budgets saved.', 'prorank-seo'),
            'budgets' => $sanitized,
        ], 200);
    }

    /**
     * Save a client-side CWV measurement.
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function save_core_web_vitals_measurement(WP_REST_Request $request): WP_REST_Response {
        $entry = [
            'url' => esc_url_raw((string) ($request->get_param('url') ?: home_url('/'))),
            'timestamp' => sanitize_text_field((string) ($request->get_param('timestamp') ?: current_time('mysql'))),
            'lcp' => $request->get_param('lcp') !== null ? (float) $request->get_param('lcp') : null,
            'inp' => $request->get_param('inp') !== null ? (float) $request->get_param('inp') : null,
            'cls' => $request->get_param('cls') !== null ? (float) $request->get_param('cls') : null,
            'fcp' => $request->get_param('fcp') !== null ? (float) $request->get_param('fcp') : null,
            'ttfb' => $request->get_param('ttfb') !== null ? (float) $request->get_param('ttfb') : null,
            'score' => $request->get_param('score') !== null ? (float) $request->get_param('score') : null,
            'device' => sanitize_key((string) ($request->get_param('device') ?: 'mobile')),
        ];

        if (!$this->is_public_measurement_url((string) $entry['url'])) {
            return new WP_REST_Response([
                'success' => false,
                'skipped' => true,
                'message' => __('Admin and internal URLs are ignored for site CWV history.', 'prorank-seo'),
            ], 200);
        }

        $history = get_option('prorank_cwv_measurements', []);
        if (!is_array($history)) {
            $history = [];
        }
        $history[] = $entry;
        if (count($history) > 500) {
            $history = array_slice($history, -500);
        }
        update_option('prorank_cwv_measurements', $history, false);

        return new WP_REST_Response([
            'success' => true,
            'message' => __('Measurement saved.', 'prorank-seo'),
        ], 200);
    }

    /**
     * Run a CWV audit across a set of URLs.
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response|\WP_Error
     */
    public function run_core_web_vitals_audit(WP_REST_Request $request) {
        if (!function_exists('prorank_has_psi_api_key')) {
            require_once PRORANK_PLUGIN_DIR . 'includes/Config/pagespeed-config.php';
        }

        if (!prorank_has_psi_api_key()) {
            return new \WP_Error(
                'psi_api_key_required',
                __('PageSpeed Insights API key is required to run a Core Web Vitals audit. Configure it in Analytics > Integrations > PageSpeed Insights.', 'prorank-seo'),
                ['status' => 400]
            );
        }

        $page_limit = max(1, min(25, (int) $request->get_param('pages')));
        $target_url = esc_url_raw((string) ($request->get_param('url') ?: home_url('/')));
        $strategy = sanitize_key((string) ($request->get_param('strategy') ?: 'mobile'));

        $urls = [$target_url];
        $posts = get_posts([
            'post_type' => ['post', 'page'],
            'post_status' => 'publish',
            'numberposts' => max(0, $page_limit - 1),
            'fields' => 'ids',
        ]);
        foreach ($posts as $post_id) {
            $permalink = get_permalink((int) $post_id);
            if ($permalink) {
                $urls[] = $permalink;
            }
        }

        $results = [];
        $with_metrics = 0;
        $without_metrics = 0;
        $errors = 0;
        foreach ($urls as $url) {
            $proxy_request = new \WP_REST_Request('POST', '/');
            $proxy_request->set_param('url', $url);
            $proxy_request->set_param('strategy', $strategy);
            $realtime = $this->get_realtime_cwv($proxy_request);

            if (is_wp_error($realtime)) {
                $errors++;
                $results[] = [
                    'url' => $url,
                    'error' => $realtime->get_error_message(),
                ];
                continue;
            }

            $payload = $realtime instanceof WP_REST_Response ? $realtime->get_data() : (array) $realtime;
            if ($this->has_metric_values(is_array($payload) ? $payload : [])) {
                $with_metrics++;
            } else {
                $without_metrics++;
            }
            $results[] = array_merge(['url' => $url], is_array($payload) ? $payload : []);
        }

        return new WP_REST_Response([
            'success' => true,
            'audited' => count($results),
            'results' => $results,
            'summary' => [
                'with_metrics' => $with_metrics,
                'without_metrics' => $without_metrics,
                'errors' => $errors,
            ],
        ], 200);
    }
}
