<?php
/**
 * TamRank GSC Connector
 *
 * Handles the OAuth connection to Google Search Console via VPS proxy.
 * All sensitive tokens are stored on the VPS - the plugin only stores connection status.
 *
 * @package TamRank
 * @since 7.3.0
 */

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

class TamRank_GSC_Connector
{
    /**
     * Singleton instance
     */
    private static $instance = null;

    /**
     * API Base URL
     */
    const API_BASE = 'https://api.tamrank.com/v1/search-console';

    /**
     * API Endpoints (relative to API_BASE)
     */
    const ENDPOINT_AUTH_START = '/auth/start';
    const ENDPOINT_AUTH_CALLBACK = '/auth/callback';
    const ENDPOINT_PROPERTIES = '/properties';
    const ENDPOINT_CONNECT = '/connect';
    const ENDPOINT_STATUS = '/status';
    const ENDPOINT_DATA = '/data';
    const ENDPOINT_DISCONNECT = '/disconnect';

    /**
     * Option keys
     */
    const OPTION_CONNECTED = 'tam_rank_gsc_connected';
    const OPTION_PROPERTY = 'tam_rank_gsc_property';
    const OPTION_LAST_SYNC = 'tam_rank_gsc_last_sync';
    const OPTION_STATE = 'tam_rank_gsc_state';
    const OPTION_PROPERTIES_CACHE = 'tam_rank_gsc_properties';

    /**
     * Transient keys for caching
     */
    const CACHE_DATA_PREFIX = 'tam_rank_gsc_data_';
    const CACHE_DURATION = 3600; // 1 hour

    /**
     * Get singleton instance
     */
    public static function get_instance()
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Constructor
     */
    private function __construct()
    {
        // Public callback handler (runs before admin_init, works even if session lost)
        add_action('init', array($this, 'handle_public_callback'));

        // Admin hooks
        add_action('admin_init', array($this, 'handle_auth_callback'));
        add_action('admin_init', array($this, 'handle_connect'));
        add_action('admin_init', array($this, 'handle_disconnect'));
        add_action('admin_init', array($this, 'handle_property_selection'));

        // AJAX handlers
        add_action('wp_ajax_tamrank_gsc_fetch_data', array($this, 'ajax_fetch_data'));
        add_action('wp_ajax_tamrank_gsc_select_property', array($this, 'ajax_select_property'));
        add_action('wp_ajax_tamrank_gsc_disconnect', array($this, 'ajax_disconnect'));
    }

    /**
     * Handle public callback from VPS OAuth redirect
     * This runs on 'init' so it works even if admin session was lost
     *
     * Supports multiple callback URL formats:
     * 1. Front-end: /?tamrank_gsc_callback=1&connected=true (preferred)
     * 2. Admin: /wp-admin/admin.php?page=tamrank&connected=true
     */
    public function handle_public_callback()
    {
        // Check for front-end callback format (preferred - works without admin session)
        $is_frontend_callback = isset($_GET['tamrank_gsc_callback']) && $_GET['tamrank_gsc_callback'] === '1'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only request inspection (admin page / OAuth callback / menu detection); no state-changing action to protect.

        // Check for admin callback format (fallback)
        $is_admin_callback = false;
        if (isset($_GET['page']) && isset($_GET['connected'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only request inspection (admin page / OAuth callback / menu detection); no state-changing action to protect.
            $page = sanitize_text_field(wp_unslash($_GET['page'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only request inspection (admin page / OAuth callback / menu detection); no state-changing action to protect.
            $is_admin_callback = in_array($page, array('tamrank', 'tam-rank', 'tam-rank-settings'), true);
        }

        // Must be one of the callback formats
        if (!$is_frontend_callback && !$is_admin_callback) {
            return;
        }

        // Check connected status
        $connected = isset($_GET['connected']) && $_GET['connected'] === 'true'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only request inspection (admin page / OAuth callback / menu detection); no state-changing action to protect.
        if (!$connected) {
            // Connection failed or was cancelled — send the user back to the
            // settings screen. The unauthenticated ?error= value is intentionally
            // not persisted (it was never surfaced, and storing it would be a
            // write triggered by an unauthenticated request).
            wp_safe_redirect(admin_url('admin.php?page=tam-rank-settings#integrations'));
            exit;
        }

        // User needs to be logged in as admin
        if (!is_user_logged_in()) {
            // Store callback data in transient so we can process after login
            set_transient('tam_rank_gsc_pending_connect', true, 300);

            // Redirect to login, then back to settings
            $redirect_url = admin_url('admin.php?page=tam-rank-settings&gsc_finalize=1');
            wp_safe_redirect(wp_login_url($redirect_url));
            exit;
        }

        if (!current_user_can('manage_options')) {
            wp_die(esc_html(tamrank__('error_permission_denied', 'You do not have permission for this action.')));
        }

        // CSRF defence-in-depth: only accept a connected=true callback when a
        // connect was actually initiated from this site (OPTION_STATE is set on
        // connect-start and cleared right below). The OAuth tokens live on the
        // VPS keyed to this site's api_key, so a forged callback can never inject
        // a foreign account — but this still blocks a cold CSRF flipping the flag.
        if (empty(get_option(self::OPTION_STATE))) {
            wp_safe_redirect(admin_url('admin.php?page=tam-rank-settings#integrations'));
            exit;
        }

        // Mark as connected (VPS already stored the tokens)
        update_option(self::OPTION_CONNECTED, true);
        update_option(self::OPTION_LAST_SYNC, current_time('mysql'));

        // Clean up state
        delete_option(self::OPTION_STATE);

        // Fetch available properties from VPS and cache them
        $properties = $this->fetch_properties();
        if (!is_wp_error($properties) && !empty($properties)) {
            update_option(self::OPTION_PROPERTIES_CACHE, $properties);

            // If only one property, auto-select it
            if (count($properties) === 1) {
                $this->select_property($properties[0]['siteUrl']);
                set_transient('tam_rank_gsc_connected', true, 60);
                do_action('tamrank_gsc_connected');
            } else {
                // Multiple properties - need user selection
                set_transient('tam_rank_gsc_select_property', true, 60);
            }
        } else {
            // Connected but couldn't fetch properties
            set_transient('tam_rank_gsc_connected', true, 60);
        }

        // Redirect to settings page (General tab where GSC card is)
        wp_safe_redirect(admin_url('admin.php?page=tam-rank-settings#integrations'));
        exit;
    }

    /**
     * Get the authorization URL to start OAuth flow
     *
     * @return string The URL to redirect the user to
     */
    /**
     * Get the authorization URL to start OAuth flow
     *
     * @return string|WP_Error The URL to redirect the user to or error
     */
    public function get_auth_url()
    {
        // GSC Connect is a FREE-tier feature. FREE-only sites have no api_key
        // until they activate a license, so auto-register them on first use.
        // Idempotent — returns existing key if already set.
        if (class_exists('\\TamRank\\LicenseManager')) {
            $license = new \TamRank\LicenseManager();
            $ensured = $license->ensure_api_key();
            if (is_wp_error($ensured)) {
                return $ensured;
            }
        }

        // Generate state nonce for CSRF protection on plugin side
        // Note: VPS creates its own state, but we can verify the roundtrip
        $local_state = wp_create_nonce('tam_rank_gsc_connect');
        update_option(self::OPTION_STATE, $local_state);

        // Fetch Auth URL from VPS
        // The VPS will extract user_uuid/site_url from the API Token
        $response = $this->api_request('GET', self::ENDPOINT_AUTH_START);

        if (is_wp_error($response)) {
            return $response;
        }

        if (empty($response['data']['auth_url'])) {
            return new WP_Error('invalid_response', tamrank__('error_invalid_server_response', 'Invalid response from server.'));
        }

        return $response['data']['auth_url'];
    }

    /**
     * Handle the OAuth callback from VPS (admin_init version)
     * Also handles finalizing connection after re-login
     */
    public function handle_auth_callback()
    {
        // GSC connection management is admin-only. This runs on admin_init for
        // any logged-in role, so gate the state-changing branches behind the cap
        // (the legitimate finalize-after-login user is the admin who connected).
        if (!current_user_can('manage_options')) {
            return;
        }

        // Handle finalize after re-login
        if (isset($_GET['page']) && $_GET['page'] === 'tam-rank-settings' && isset($_GET['gsc_finalize'])) {
            // CSRF defence-in-depth: only finalize when a connect was actually
            // initiated from this site. OPTION_STATE holds the connect-start nonce
            // (set in get_auth_url(), reachable only through the admin connect
            // action). A pending-connect transient on its own can be planted by an
            // unauthenticated visitor, so it is not sufficient proof of intent.
            $saved_state = get_option(self::OPTION_STATE);
            $state_valid = !empty($saved_state) && wp_verify_nonce($saved_state, 'tam_rank_gsc_connect');

            if ($state_valid && get_transient('tam_rank_gsc_pending_connect')) {
                delete_transient('tam_rank_gsc_pending_connect');

                // Mark as connected
                update_option(self::OPTION_CONNECTED, true);
                update_option(self::OPTION_LAST_SYNC, current_time('mysql'));
                delete_option(self::OPTION_STATE);

                // Set success message
                set_transient('tam_rank_gsc_connected', true, 60);

                // Redirect to clean URL
                wp_safe_redirect(admin_url('admin.php?page=tam-rank-settings#integrations'));
                exit;
            }
        }

        // Legacy callback handler (kept for backwards compatibility)
        if (!isset($_GET['page']) || $_GET['page'] !== 'tam-rank-settings') {
            return;
        }

        if (!isset($_GET['gsc_auth']) || $_GET['gsc_auth'] !== 'success') {
            return;
        }

        // Verify state nonce
        $saved_state = get_option(self::OPTION_STATE);
        $received_state = isset($_GET['state']) ? sanitize_text_field(wp_unslash($_GET['state'])) : '';

        if (empty($saved_state) || !wp_verify_nonce($received_state, 'tam_rank_gsc_connect')) {
            add_settings_error(
                'tam_rank_gsc',
                'invalid_state',
                tamrank__('error_security_check_failed', 'Security check failed. Please try again.'),
                'error'
            );
            return;
        }

        // Clean up state
        delete_option(self::OPTION_STATE);

        // Fetch available properties from VPS
        $properties = $this->fetch_properties();

        if (is_wp_error($properties)) {
            add_settings_error(
                'tam_rank_gsc',
                'fetch_error',
                tamrank__('error_fetch_gsc_properties', 'Could not fetch Search Console properties: ') . $properties->get_error_message(),
                'error'
            );
            return;
        }

        if (empty($properties)) {
            add_settings_error(
                'tam_rank_gsc',
                'no_properties',
                tamrank__('error_no_gsc_properties', 'No Search Console properties found for this Google account.'),
                'warning'
            );
            return;
        }

        // Store properties for selection
        update_option(self::OPTION_PROPERTIES_CACHE, $properties);

        // If only one property, auto-select it
        if (count($properties) === 1) {
            $this->select_property($properties[0]['siteUrl']);
            add_settings_error(
                'tam_rank_gsc',
                'connected',
                tamrank__('gsc_connected_success', 'Successfully connected to Google Search Console!'),
                'success'
            );
        } else {
            add_settings_error(
                'tam_rank_gsc',
                'select_property',
                tamrank__('gsc_select_property_prompt', 'Please select a Search Console property to connect.'),
                'info'
            );
        }

        // Redirect to clean URL
        wp_safe_redirect(admin_url('admin.php?page=tam-rank-settings#integrations'));
        exit;
    }

    /**
     * Handle property selection from form
     */
    public function handle_property_selection()
    {
        if (!isset($_POST['tam_rank_gsc_select_property'])) {
            return;
        }

        if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'] ?? '')), 'tam_rank_gsc_select_property')) {
            wp_die(esc_html(tamrank__('error_security_token', 'Invalid security token. Please refresh the page.')));
        }

        if (!current_user_can('manage_options')) {
            wp_die(esc_html(tamrank__('error_permission_denied', 'You do not have permission for this action.')));
        }

        // Use sanitize_text_field instead of sanitize_url because GSC domain properties
        // use format "sc-domain:example.com" which is not a valid URL format
        $property_url = sanitize_text_field(wp_unslash($_POST['gsc_property'] ?? ''));

        if (empty($property_url)) {
            add_settings_error(
                'tam_rank_gsc',
                'no_property',
                tamrank__('gsc_please_select_property', 'Please select a property.'),
                'error'
            );
            return;
        }

        // Validate it's either a URL or sc-domain format
        if (strpos($property_url, 'sc-domain:') !== 0 && !filter_var($property_url, FILTER_VALIDATE_URL)) {
            add_settings_error(
                'tam_rank_gsc',
                'invalid_property',
                tamrank__('error_invalid_property_format', 'Invalid property format.'),
                'error'
            );
            return;
        }

        $result = $this->select_property($property_url);

        if (is_wp_error($result)) {
            add_settings_error(
                'tam_rank_gsc',
                'select_error',
                $result->get_error_message(),
                'error'
            );
        } else {
            add_settings_error(
                'tam_rank_gsc',
                'connected',
                tamrank__('gsc_connected_success', 'Successfully connected to Google Search Console!'),
                'success'
            );
        }
    }

    /**
     * Handle disconnect request
     */
    public function handle_disconnect()
    {
        if (!isset($_GET['action']) || $_GET['action'] !== 'tam_rank_gsc_disconnect') {
            return;
        }

        if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'tam_rank_gsc_disconnect')) {
            wp_die(esc_html(tamrank__('error_security_token', 'Invalid security token. Please refresh the page.')));
        }

        if (!current_user_can('manage_options')) {
            wp_die(esc_html(tamrank__('error_permission_denied', 'You do not have permission for this action.')));
        }

        $this->disconnect();

        wp_safe_redirect(admin_url('admin.php?page=tam-rank-settings&gsc_disconnected=1#integrations'));
        exit;
    }

    /**
     * Handle the "Connect with Google" click.
     *
     * The network work — site registration (if this site has no api_key yet)
     * and fetching the OAuth URL from the VPS — runs ONLY here, on an explicit
     * nonce-verified user action, never on page render. This keeps the plugin
     * from contacting api.tamrank.com just because the settings screen loaded
     * (wp.org guideline 7: no external requests without explicit user consent).
     */
    public function handle_connect()
    {
        if (!isset($_GET['action']) || $_GET['action'] !== 'tam_rank_gsc_connect') {
            return;
        }

        if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'tam_rank_gsc_connect_init')) {
            wp_die(esc_html(tamrank__('error_security_token', 'Invalid security token. Please refresh the page.')));
        }

        if (!current_user_can('manage_options')) {
            wp_die(esc_html(tamrank__('error_permission_denied', 'You do not have permission for this action.')));
        }

        // Explicit user retry: clear any recent register/verify back-off so the
        // ownership-proof handshake re-attempts now instead of returning a cached
        // failure state.
        if (class_exists('\\TamRank\\LicenseManager')) {
            ( new \TamRank\LicenseManager() )->clear_register_backoff();
        }

        $auth_url = $this->get_auth_url();

        if (is_wp_error($auth_url)) {
            // Surface the real reason back on the settings screen so testers and
            // users can see VPS/network/license issues instead of a dead button.
            // 'data' carries ownership-verification detail (checked_url, reason)
            // when the code is 'verify_failed', for the friendly notice.
            set_transient('tam_rank_gsc_connect_error', array(
                'code' => $auth_url->get_error_code(),
                'msg'  => $auth_url->get_error_message(),
                'data' => $auth_url->get_error_data(),
            ), 60);
            wp_safe_redirect(admin_url('admin.php?page=tam-rank-settings#integrations'));
            exit;
        }

        // phpcs:ignore WordPress.Security.SafeRedirect -- $auth_url is the external Google OAuth consent URL; wp_safe_redirect() would rewrite it to the site host and break the connect flow.
        wp_redirect($auth_url);
        exit;
    }

    /**
     * Select and store a property
     *
     * @param string $property_url The property URL to connect
     * @return bool|WP_Error
     */
    public function select_property($property_url)
    {
        // Notify VPS of selection
        $response = $this->api_request('POST', self::ENDPOINT_CONNECT, array(
            'property_url' => $property_url,
        ));

        if (is_wp_error($response)) {
            return $response;
        }

        // Store locally
        update_option(self::OPTION_PROPERTY, $property_url);
        update_option(self::OPTION_CONNECTED, true);
        update_option(self::OPTION_LAST_SYNC, current_time('mysql'));

        // Clear properties cache
        delete_option(self::OPTION_PROPERTIES_CACHE);

        // Notify listeners (e.g. Index Checker kicks off initial scan)
        do_action('tamrank_gsc_connected');

        return true;
    }

    /**
     * Fetch available properties from VPS
     *
     * @return array|WP_Error Array of properties or error
     */
    public function fetch_properties()
    {
        $response = $this->api_request('GET', self::ENDPOINT_PROPERTIES);

        if (is_wp_error($response)) {
            return $response;
        }

        // Extract properties array from response
        // Try different possible response formats:
        $properties = null;

        // Format 1: {"success": true, "data": {"properties": [...]}}
        if (isset($response['data']['properties']) && is_array($response['data']['properties'])) {
            $properties = $response['data']['properties'];
        }
        // Format 2: {"success": true, "data": [{"siteUrl": "..."}, ...]} (array of properties directly)
        elseif (isset($response['data']) && is_array($response['data']) && !empty($response['data'])) {
            // Check if data is a sequential array of property objects (not associative)
            $first_key = array_key_first($response['data']);
            if (is_int($first_key)) {
                $first = $response['data'][$first_key];
                if (is_array($first) && (isset($first['siteUrl']) || isset($first['site_url']))) {
                    $properties = $response['data'];
                }
            }
        }
        // Format 3: {"properties": [...]}
        elseif (isset($response['properties']) && is_array($response['properties'])) {
            $properties = $response['properties'];
        }
        // Format 4: Direct array [{"siteUrl": "..."}, ...]
        elseif (is_array($response) && !empty($response)) {
            $first_key = array_key_first($response);
            if (is_int($first_key)) {
                $first = $response[$first_key];
                if (is_array($first) && (isset($first['siteUrl']) || isset($first['site_url']))) {
                    $properties = $response;
                }
            }
        }

        if ($properties !== null) {
            // Validate and normalize the properties array
            $validated = array();
            foreach ($properties as $prop) {
                if (is_array($prop) && (isset($prop['siteUrl']) || isset($prop['site_url']))) {
                    $validated[] = $prop;
                }
            }
            return $validated;
        }

        return new WP_Error('invalid_response', tamrank__('error_invalid_properties_response', 'Invalid properties response from server.'));
    }

    /**
     * Get cached properties (for property selection UI)
     *
     * @return array
     */
    public function get_cached_properties()
    {
        return get_option(self::OPTION_PROPERTIES_CACHE, array());
    }


    /**
     * Fetch GSC data for a specific date range
     *
     * @param int $days Number of days (7, 28, 90, etc.)
     * @param bool $force_refresh Bypass cache
     * @return array|WP_Error
     */
    public function fetch_data($days = 7, $force_refresh = false)
    {
        // Check for mock mode
        if (defined('TAMRANK_GSC_MOCK') && TAMRANK_GSC_MOCK) {
            return $this->get_mock_data($days);
        }

        // Check cache
        $cache_key = self::CACHE_DATA_PREFIX . $days;
        if (!$force_refresh) {
            $cached = get_transient($cache_key);
            if ($cached !== false) {
                return $cached;
            }
        }

        // Calculate date range (VPS expects start_date and end_date)
        $end_date = gmdate('Y-m-d', strtotime('-1 day')); // Yesterday (GSC data has 2-day delay)
        $start_date = gmdate('Y-m-d', strtotime("-{$days} days"));

        // Fetch from VPS
        $response = $this->api_request('GET', self::ENDPOINT_DATA, array(
            'start_date' => $start_date,
            'end_date' => $end_date,
        ));

        if (is_wp_error($response)) {
            // Return cached data if available (stale cache fallback)
            $stale = get_transient($cache_key . '_stale');
            if ($stale !== false) {
                $stale['_stale'] = true;
                return $stale;
            }
            return $response;
        }

        // Transform VPS response to dashboard format
        $transformed = $this->transform_gsc_data($response, $days);

        // Cache the transformed response
        set_transient($cache_key, $transformed, self::CACHE_DURATION);
        set_transient($cache_key . '_stale', $transformed, self::CACHE_DURATION * 4); // 4 hour stale cache

        // Update last sync
        update_option(self::OPTION_LAST_SYNC, current_time('mysql'));

        return $transformed;
    }

    /**
     * Transform VPS GSC data to dashboard format
     *
     * VPS returns: {"data": {"rows": [{date, clicks, impressions, ctr, position}, ...]}}
     * Dashboard expects: {"totals": {...}, "changes": {...}, "series": [...]}
     *
     * @param array $response VPS response
     * @param int $days Number of days for comparison
     * @return array Transformed data
     */
    private function transform_gsc_data($response, $days)
    {
        // Extract rows from response
        $rows = array();
        if (isset($response['data']['rows'])) {
            $rows = $response['data']['rows'];
        } elseif (isset($response['rows'])) {
            $rows = $response['rows'];
        } elseif (isset($response['data']) && is_array($response['data'])) {
            // Maybe data is directly the rows array
            $rows = $response['data'];
        }

        // If no rows, return empty structure
        if (empty($rows)) {
            return array(
                'totals' => array('clicks' => 0, 'impressions' => 0, 'ctr' => 0, 'position' => 0),
                'changes' => array('clicks' => 0, 'impressions' => 0, 'ctr' => 0, 'position' => 0),
                'series' => array(),
            );
        }

        // Sort rows by date
        usort($rows, function ($a, $b) {
            return strcmp($a['date'] ?? '', $b['date'] ?? '');
        });

        // Calculate totals
        $total_clicks = 0;
        $total_impressions = 0;
        $total_ctr = 0;
        $total_position = 0;
        $count = count($rows);

        foreach ($rows as $row) {
            $total_clicks += $row['clicks'] ?? 0;
            $total_impressions += $row['impressions'] ?? 0;
            // Google/VPS sends CTR as decimal (0.0375 = 3.75%), convert to percentage
            $ctr_value = (float) ($row['ctr'] ?? 0);
            if ($ctr_value > 0 && $ctr_value < 1) {
                $ctr_value = $ctr_value * 100;
            }
            $total_ctr += $ctr_value;
            $total_position += $row['position'] ?? 0;
        }

        $avg_ctr = $count > 0 ? round($total_ctr / $count, 2) : 0;
        $avg_position = $count > 0 ? round($total_position / $count, 1) : 0;

        // Calculate changes (compare first half to second half)
        $half = intval($count / 2);
        $first_half = array_slice($rows, 0, $half);
        $second_half = array_slice($rows, $half);

        $first_clicks = array_sum(array_column($first_half, 'clicks'));
        $second_clicks = array_sum(array_column($second_half, 'clicks'));
        $first_impressions = array_sum(array_column($first_half, 'impressions'));
        $second_impressions = array_sum(array_column($second_half, 'impressions'));

        // Helper to convert CTR: if < 1, multiply by 100 (decimal to percentage)
        $convert_ctr = function ($ctr) {
            return ($ctr > 0 && $ctr < 1) ? $ctr * 100 : $ctr;
        };

        // Calculate average CTR for each half (with conversion)
        $first_ctr_sum = 0;
        foreach ($first_half as $row) {
            $first_ctr_sum += $convert_ctr($row['ctr'] ?? 0);
        }
        $first_ctr = count($first_half) > 0 ? $first_ctr_sum / count($first_half) : 0;

        $second_ctr_sum = 0;
        foreach ($second_half as $row) {
            $second_ctr_sum += $convert_ctr($row['ctr'] ?? 0);
        }
        $second_ctr = count($second_half) > 0 ? $second_ctr_sum / count($second_half) : 0;

        $first_position = count($first_half) > 0 ? array_sum(array_column($first_half, 'position')) / count($first_half) : 0;
        $second_position = count($second_half) > 0 ? array_sum(array_column($second_half, 'position')) / count($second_half) : 0;

        // Calculate percentage changes
        $clicks_change = $first_clicks > 0 ? round((($second_clicks - $first_clicks) / $first_clicks) * 100, 1) : 0;
        $impressions_change = $first_impressions > 0 ? round((($second_impressions - $first_impressions) / $first_impressions) * 100, 1) : 0;
        $ctr_change = round($second_ctr - $first_ctr, 2);
        $position_change = round($second_position - $first_position, 1);

        // Convert CTR values in series data for consistency
        $normalized_rows = array_map(function ($row) use ($convert_ctr) {
            $row['ctr'] = round($convert_ctr($row['ctr'] ?? 0), 2);
            return $row;
        }, $rows);

        return array(
            'totals' => array(
                'clicks' => $total_clicks,
                'impressions' => $total_impressions,
                'ctr' => $avg_ctr,
                'position' => $avg_position,
            ),
            'changes' => array(
                'clicks' => $clicks_change,
                'impressions' => $impressions_change,
                'ctr' => $ctr_change,
                'position' => $position_change,
            ),
            'series' => $normalized_rows,
        );
    }

    /**
     * Get cached summary data for dashboard widget
     *
     * @return array
     */
    public function get_cached_summary()
    {
        $data = $this->fetch_data(7);

        if (is_wp_error($data)) {
            return array(
                'clicks' => 0,
                'impressions' => 0,
                'ctr' => 0,
                'position' => 0,
                'error' => true,
            );
        }

        return array(
            'clicks' => $data['totals']['clicks'] ?? 0,
            'impressions' => $data['totals']['impressions'] ?? 0,
            'ctr' => $data['totals']['ctr'] ?? 0,
            'position' => $data['totals']['position'] ?? 0,
            'clicks_change' => $data['changes']['clicks'] ?? 0,
            'impressions_change' => $data['changes']['impressions'] ?? 0,
            'ctr_change' => $data['changes']['ctr'] ?? 0,
            'position_change' => $data['changes']['position'] ?? 0,
            'error' => false,
            '_stale' => $data['_stale'] ?? false,
        );
    }

    /**
     * Disconnect from GSC
     *
     * @return bool|WP_Error
     */
    public function disconnect()
    {
        // Notify VPS to revoke token
        $this->api_request('DELETE', self::ENDPOINT_DISCONNECT);

        // Clear local data
        delete_option(self::OPTION_CONNECTED);
        delete_option(self::OPTION_PROPERTY);
        delete_option(self::OPTION_LAST_SYNC);
        delete_option(self::OPTION_STATE);
        delete_option(self::OPTION_PROPERTIES_CACHE);

        // Clear all cached data
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- direct query on the core options table to purge this plugin's cached GSC transients; admin-context write on disconnect, caching not applicable
        $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
                '_transient_' . self::CACHE_DATA_PREFIX . '%'
            )
        );

        return true;
    }


    /**
     * Check if connected to GSC
     *
     * @return bool
     */
    public static function is_connected()
    {
        return (bool) get_option(self::OPTION_CONNECTED, false);
    }

    /**
     * Check if in property selection state
     * Returns true when connected to Google but no property selected yet
     *
     * @return bool
     */
    public function needs_property_selection()
    {
        // Need selection when: connected to Google BUT no property selected
        return self::is_connected() && empty(self::get_property());
    }

    /**
     * Get connected property URL
     *
     * @return string|null
     */
    public static function get_property()
    {
        return get_option(self::OPTION_PROPERTY, null);
    }

    /**
     * Get last sync time
     *
     * @return string|null
     */
    public static function get_last_sync()
    {
        return get_option(self::OPTION_LAST_SYNC, null);
    }

    /**
     * AJAX handler: Fetch GSC data
     */
    /**
     * AJAX handler: Fetch GSC data
     */
    public function ajax_fetch_data()
    {
        // Verify nonce
        if (!check_ajax_referer('tamrank_gsc_nonce', 'nonce', false)) {
            wp_send_json_error(array('message' => tamrank__('error_security_token', 'Invalid security token. Please refresh the page.'), 'code' => 'nonce_invalid'));
        }

        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => tamrank__('error_permission_denied', 'You do not have permission for this action.'), 'code' => 'permission_denied'));
        }

        // Validate the requested range against the known set of date windows.
        // This is a sanity bound only — actual data entitlement is decided by the
        // backend (api.tamrank.com), which authorises each request per site. The
        // PRO add-on extends the dashboard dropdown via the
        // tamrank_gsc_available_periods filter; no feature is gated here.
        $range = isset($_POST['range']) ? absint($_POST['range']) : 28;
        // Derive the accepted set from the same filter that builds the dashboard
        // dropdown, so FREE (7/28) and PRO (wider) validate in lockstep.
        $known_ranges = apply_filters('tamrank_gsc_available_periods', array(7, 28));
        if (!in_array($range, $known_ranges, true)) {
            $range = 28;
        }

        $force = isset($_POST['force']) && $_POST['force'] === 'true';

        $data = $this->fetch_data($range, $force);

        if (is_wp_error($data)) {
            wp_send_json_error(array(
                'message' => $data->get_error_message(),
                'code' => $data->get_error_code(),
            ));
        }

        // Ensure data structure is correct before sending
        if (!isset($data['totals']) || !isset($data['series'])) {
            // If the API returns a success but invalid structure, log it
            wp_send_json_error(array('message' => tamrank__('error_invalid_data', 'Invalid data received from the server.'), 'code' => 'request_failed'));
        }

        wp_send_json_success($data);
    }

    /**
     * AJAX handler: Select property
     */
    public function ajax_select_property()
    {
        // Accept both nonce formats for flexibility
        if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'tamrank_gsc_select_property_ajax') &&
            !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'tamrank_gsc_nonce')) {
            wp_send_json_error(array('message' => tamrank__('error_security_token', 'Invalid security token. Please refresh the page.'), 'code' => 'nonce_invalid'));
        }

        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => tamrank__('error_permission_denied', 'You do not have permission for this action.'), 'code' => 'permission_denied'));
        }

        // Use sanitize_text_field instead of sanitize_url because GSC domain properties
        // use format "sc-domain:example.com" which is not a valid URL format
        $property_url = isset($_POST['property']) ? sanitize_text_field(wp_unslash($_POST['property'])) : '';

        if (empty($property_url)) {
            wp_send_json_error(array('message' => tamrank__('error_no_property', 'No property selected.'), 'code' => 'missing_parameter'));
        }

        // Validate it's either a URL or sc-domain format
        if (strpos($property_url, 'sc-domain:') !== 0 && !filter_var($property_url, FILTER_VALIDATE_URL)) {
            wp_send_json_error(array('message' => tamrank__('error_invalid_property', 'Invalid property format.'), 'code' => 'missing_parameter'));
        }

        $result = $this->select_property($property_url);

        if (is_wp_error($result)) {
            wp_send_json_error(array('message' => $result->get_error_message(), 'code' => $result->get_error_code() ?: 'request_failed'));
        }

        wp_send_json_success(array('message' => tamrank__('gsc_property_connected', 'Property connected successfully!')));
    }

    /**
     * AJAX handler: Disconnect
     */
    public function ajax_disconnect()
    {
        check_ajax_referer('tamrank_gsc_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => tamrank__('error_permission_denied', 'You do not have permission for this action.'), 'code' => 'permission_denied'));
        }

        $this->disconnect();

        wp_send_json_success(array('message' => tamrank__('gsc_disconnected', 'Disconnected from Search Console.')));
    }

    /**
     * Make API request to VPS
     *
     * @param string $method HTTP method
     * @param string $endpoint API endpoint
     * @param array $data Request data
     * @return array|WP_Error
     */
    private function api_request($method, $endpoint, $data = array())
    {
        $api_key = $this->get_api_key();

        if (empty($api_key)) {
            return new WP_Error('no_api_key', tamrank__('error_api_key_not_configured', 'TamRank API key not configured. Go to Settings → TamRank.'));
        }

        $url = self::API_BASE . $endpoint;

        // Add query params for GET requests
        if ($method === 'GET' && !empty($data)) {
            $url = add_query_arg($data, $url);
        }

        $args = array(
            'method' => $method,
            'timeout' => 30,
            'headers' => array(
                'Authorization' => 'Bearer ' . $api_key,
                'Content-Type' => 'application/json',
                'Accept' => 'application/json',
                'X-Site-URL' => site_url(),
                'X-Plugin-Version' => defined('TAM_RANK_VERSION') ? TAM_RANK_VERSION : '1.0.0',
            ),
        );

        // Add body for POST/PUT/DELETE
        if (in_array($method, array('POST', 'PUT', 'DELETE')) && !empty($data)) {
            $args['body'] = wp_json_encode($data);
        }

        $response = wp_remote_request($url, $args);

        // Network error
        if (is_wp_error($response)) {
            return new WP_Error('network_error', tamrank__('error_connection_failed', 'Connection to server failed.'));
        }

        $status_code = wp_remote_retrieve_response_code($response);
        $body = wp_remote_retrieve_body($response);
        $data = json_decode($body, true);

        // Success
        if ($status_code >= 200 && $status_code < 300) {
            return $data;
        }

        // Error handling based on status code
        // Try multiple common error field names
        $error_message = '';
        if (isset($data['message'])) {
            $error_message = $data['message'];
        } elseif (isset($data['error'])) {
            $error_message = is_array($data['error']) ? ($data['error']['message'] ?? wp_json_encode($data['error'])) : $data['error'];
        } elseif (isset($data['error_description'])) {
            $error_message = $data['error_description'];
        }

        // Log the full response for debugging

        switch ($status_code) {
            case 401:
                return new WP_Error('unauthorized', tamrank__('error_auth_failed', 'Authentication failed. Please check your API key.'));

            case 403:
                return new WP_Error('forbidden', tamrank__('error_no_access_feature', 'No access to this feature.'));

            case 404:
                return new WP_Error('not_found', tamrank__('error_gsc_endpoint_unavailable', 'GSC endpoint not available on the server.'));

            case 429:
                return new WP_Error('rate_limited', tamrank__('error_too_many_requests', 'Too many requests. Please try again later.'));

            case 500:
            case 502:
            case 503:
                return new WP_Error('server_error', tamrank__('error_server_unavailable', 'Server temporarily unavailable.'));

            default:
                // Provide more context in the error message
                $default_msg = $error_message ?: sprintf(tamrank__('error_server_code', 'Server error (code %d)'), $status_code);
                return new WP_Error('api_error', $default_msg);
        }
    }

    /**
     * Get API key from license
     *
     * @return string|null
     */
    private function get_api_key()
    {
        // Get API key directly from WordPress options.
        // This is stored by LicenseManager during activation.
        // Defensive fallback to get_option() — this method gets called from
        // the OAuth callback on the 'init' hook, which can fire before
        // helpers.php is loaded if registration order ever drifts.
        $api_key = function_exists('tam_rank_cached_option')
            ? tam_rank_cached_option('tam_rank_api_key', '')
            : get_option('tam_rank_api_key', '');
        return !empty($api_key) ? $api_key : null;
    }

    /**
     * Get mock data for testing without VPS connection
     *
     * @param int $days Number of days
     * @return array
     */
    private function get_mock_data($days)
    {
        $series = array();
        $total_clicks = 0;
        $total_impressions = 0;

        // Generate realistic mock data
        $base_clicks = wp_rand(50, 150);
        $base_impressions = wp_rand(1000, 3000);

        for ($i = $days - 1; $i >= 0; $i--) {
            $date = gmdate('Y-m-d', strtotime("-{$i} days"));

            // Add some variance
            $variance = (wp_rand(-20, 20) / 100);
            $clicks = round($base_clicks * (1 + $variance));
            $impressions = round($base_impressions * (1 + $variance));

            $total_clicks += $clicks;
            $total_impressions += $impressions;

            $series[] = array(
                'date' => $date,
                'clicks' => $clicks,
                'impressions' => $impressions,
                'ctr' => round(($clicks / $impressions) * 100, 2),
                'position' => round(wp_rand(80, 200) / 10, 1),
            );
        }

        $avg_ctr = $total_impressions > 0 ? round(($total_clicks / $total_impressions) * 100, 2) : 0;
        $avg_position = round(wp_rand(100, 180) / 10, 1);

        return array(
            'series' => $series,
            'totals' => array(
                'clicks' => $total_clicks,
                'impressions' => $total_impressions,
                'ctr' => $avg_ctr,
                'position' => $avg_position,
            ),
            'changes' => array(
                'clicks' => round(wp_rand(-15, 25), 1),
                'impressions' => round(wp_rand(-10, 20), 1),
                'ctr' => round(wp_rand(-5, 10) / 10, 2),
                'position' => round(wp_rand(-20, 10) / 10, 1),
            ),
            '_mock' => true,
        );
    }
}

// Initialize singleton
TamRank_GSC_Connector::get_instance();
