<?php

namespace SerpIQ\Admin;

use SerpIQ\Gsc\Backfill;
use SerpIQ\Gsc\OAuth;
use SerpIQ\Gsc\Property_Detector;
use SerpIQ\Gsc\Sync;
use SerpIQ\Helpers\Options;

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

final class Admin {
	private const SETTINGS_UI_NONCE_ACTION = 'seotune_settings_ui_get';

	private static ?Admin $instance = null;

	public static function instance(): Admin {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	public function init(): void {
		add_action( 'admin_menu', array( $this, 'register_menu' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
	}

	public function register_menu(): void {
		if ( ! current_user_can( SERPIQ_CAPABILITY ) ) {
			return;
		}

		// Menu icon must be shipped in production zip (admin/assets is included).
		$menu_icon = SERPIQ_PLUGIN_URL . 'admin/assets/brand/menu-icon.png';

		$hook = add_menu_page(
			__( 'Seotune Search Insights', 'seotune-search-insights' ),
			__( 'Seotune', 'seotune-search-insights' ),
			SERPIQ_CAPABILITY,
			'serpiq',
			array( $this, 'render_app_root' ),
			$menu_icon,
			56
		);

		add_submenu_page( 'serpiq', __( 'Dashboard', 'seotune-search-insights' ), __( 'Dashboard', 'seotune-search-insights' ), SERPIQ_CAPABILITY, 'serpiq' );
		add_submenu_page( 'serpiq', __( 'Keywords', 'seotune-search-insights' ), __( 'Keywords', 'seotune-search-insights' ), SERPIQ_CAPABILITY, 'serpiq-keywords', array( $this, 'render_app_root' ) );
		add_submenu_page( 'serpiq', __( 'Pages', 'seotune-search-insights' ), __( 'Pages', 'seotune-search-insights' ), SERPIQ_CAPABILITY, 'serpiq-pages', array( $this, 'render_app_root' ) );
		add_submenu_page( 'serpiq', __( 'Opportunities', 'seotune-search-insights' ), __( 'Opportunities', 'seotune-search-insights' ), SERPIQ_CAPABILITY, 'serpiq-opportunities', array( $this, 'render_app_root' ) );
		add_submenu_page( 'serpiq', __( 'Cannibalization', 'seotune-search-insights' ), __( 'Cannibalization', 'seotune-search-insights' ), SERPIQ_CAPABILITY, 'serpiq-cannibalization', array( $this, 'render_app_root' ) );
		add_submenu_page( 'serpiq', __( 'Decay', 'seotune-search-insights' ), __( 'Decay', 'seotune-search-insights' ), SERPIQ_CAPABILITY, 'serpiq-decay', array( $this, 'render_app_root' ) );
		add_submenu_page( 'serpiq', __( 'Content Refresh', 'seotune-search-insights' ), __( 'Content Refresh', 'seotune-search-insights' ), SERPIQ_CAPABILITY, 'serpiq-content-refresh', array( $this, 'render_app_root' ) );
		add_submenu_page( 'serpiq', __( 'Internal Links', 'seotune-search-insights' ), __( 'Internal Links', 'seotune-search-insights' ), SERPIQ_CAPABILITY, 'serpiq-internal-links', array( $this, 'render_app_root' ) );
		add_submenu_page( 'serpiq', __( 'Question Queries', 'seotune-search-insights' ), __( 'Question Queries', 'seotune-search-insights' ), SERPIQ_CAPABILITY, 'serpiq-question-queries', array( $this, 'render_app_root' ) );
		$settings_hook = add_submenu_page( 'serpiq', __( 'Settings', 'seotune-search-insights' ), __( 'Settings', 'seotune-search-insights' ), SERPIQ_CAPABILITY, 'serpiq-settings', array( $this, 'render_settings_page' ) );

		add_action( 'load-' . $hook, array( $this, 'maybe_handle_admin_actions' ) );
		add_action( 'load-' . $settings_hook, array( $this, 'maybe_handle_admin_actions' ) );
	}

	/**
	 * Scalar GET helper (unchecksummed). Use get_settings_ui_get_string() for Settings UI query args (M-8).
	 */
	private function get_admin_get_string( string $key ): string {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- page=… routing; Settings UI uses get_settings_ui_get_string + SETTINGS_UI_NONCE_ACTION.
		if ( ! isset( $_GET[ $key ] ) || ! is_scalar( $_GET[ $key ] ) ) {
			$out = '';
		} else {
			$out = sanitize_text_field( (string) wp_unslash( $_GET[ $key ] ) );
		}
		// phpcs:enable WordPress.Security.NonceVerification.Recommended

		return $out;
	}

	/**
	 * Reads Google OAuth 2.0 callback query parameters (code, state).
	 *
	 * Google does not append WordPress _wpnonce; CSRF for this step is OAuth state
	 * validation inside OAuth::handle_oauth_callback() (RFC 6749).
	 *
	 * @return array{code:string,state:string}|null
	 */
	private function get_oauth_callback_get_params(): ?array {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Google OAuth redirect; CSRF mitigated via state validation in OAuth::handle_oauth_callback().
		// OAuth callback is only meaningful for a logged-in admin who initiated the flow.
		if ( ! is_user_logged_in() ) {
			return null;
		}
		if ( ! isset( $_GET['code'], $_GET['state'] ) ) {
			$out = null;
		} elseif ( ! is_scalar( $_GET['code'] ) || ! is_scalar( $_GET['state'] ) ) {
			$out = null;
		} else {
			$out = array(
				'code'  => sanitize_text_field( (string) wp_unslash( $_GET['code'] ) ),
				'state' => sanitize_text_field( (string) wp_unslash( $_GET['state'] ) ),
			);
		}
		// phpcs:enable WordPress.Security.NonceVerification.Recommended

		return $out;
	}

	private function is_settings_ui_get_verified(): bool {
		if ( ! isset( $_GET['_wpnonce'] ) || ! is_scalar( $_GET['_wpnonce'] ) ) {
			return false;
		}

		return (bool) wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), self::SETTINGS_UI_NONCE_ACTION );
	}

	/**
	 * Settings GET params that drive UI (tab, notices, diagnostics): only read after nonce check (M-8).
	 */
	private function get_settings_ui_get_string( string $key ): string {
		if ( ! $this->is_settings_ui_get_verified() ) {
			return '';
		}

		return $this->get_admin_get_string( $key );
	}

	/**
	 * Sign Settings screen GET URLs (tab / notices / diagnostics flags).
	 *
	 * For internal redirects only. Do not use for Google OAuth authorize URLs:
	 * OAuth CSRF is handled by the standard state parameter on callback, not _wpnonce.
	 *
	 * @param array<string, scalar> $args
	 */
	private function build_settings_ui_url( string $base_url, array $args = array() ): string {
		$url = $base_url;
		foreach ( $args as $key => $value ) {
			$url = add_query_arg( (string) $key, $value, $url );
		}

		return add_query_arg( '_wpnonce', wp_create_nonce( self::SETTINGS_UI_NONCE_ACTION ), $url );
	}

	public function maybe_handle_admin_actions(): void {
		if ( ! current_user_can( SERPIQ_CAPABILITY ) ) {
			return;
		}

		$page = sanitize_key( $this->get_admin_get_string( 'page' ) );
		if ( 'serpiq-settings' !== $page ) {
			return;
		}

		// M-11: state-changing branches below — capability gate above; each branch uses either
		// check_admin_referer( …, 'seotune_nonce' ) for POST, OAuth GET nonces, OAuth callback
		// code+state (no WP URL nonce on Google return), or redirects signed via build_settings_ui_url().

		// Save settings.
		if ( isset( $_POST['serpiq_settings_save'] ) ) {
			check_admin_referer( 'seotune_settings_save', 'seotune_nonce' );

			$client_id = isset( $_POST['serpiq_google_client_id'] ) ? sanitize_text_field( (string) wp_unslash( $_POST['serpiq_google_client_id'] ) ) : '';
			$client_secret = isset( $_POST['serpiq_google_client_secret'] ) ? sanitize_text_field( (string) wp_unslash( $_POST['serpiq_google_client_secret'] ) ) : '';
			$delete_on_uninstall = isset( $_POST['serpiq_delete_data_on_uninstall'] ) ? 1 : 0;

			update_option( Options::google_client_id_key(), $client_id, false );
			update_option( Options::google_client_secret_key(), $client_secret, false );
			update_option( Options::delete_data_on_uninstall_key(), $delete_on_uninstall, false );

			wp_safe_redirect( $this->build_settings_ui_url( menu_page_url( 'serpiq-settings', false ), array( 'serpiq_notice' => 'saved' ) ) );
			exit;
		}

		// Save advanced import settings (Sync & Import tab).
		if ( isset( $_POST['serpiq_advanced_import_save'] ) ) {
			check_admin_referer( 'seotune_save_advanced_import_settings', 'seotune_nonce' );

			$batch_days = isset( $_POST['serpiq_backfill_batch_days'] ) ? absint( wp_unslash( $_POST['serpiq_backfill_batch_days'] ) ) : 0;
			if ( $batch_days <= 0 ) {
				$batch_days = 3;
			}
			if ( $batch_days < 1 ) {
				$batch_days = 1;
			} elseif ( $batch_days > 30 ) {
				$batch_days = 30;
			}

			$delay = isset( $_POST['serpiq_backfill_delay_seconds'] ) ? absint( wp_unslash( $_POST['serpiq_backfill_delay_seconds'] ) ) : 60;
			$allowed = array( 10, 30, 60 );
			if ( ! in_array( $delay, $allowed, true ) ) {
				$delay = 60;
			}

			$db_batch_size = isset( $_POST['serpiq_db_batch_size'] ) ? absint( wp_unslash( $_POST['serpiq_db_batch_size'] ) ) : 0;
			if ( $db_batch_size <= 0 ) {
				$db_batch_size = 500;
			}
			if ( $db_batch_size < 100 ) {
				$db_batch_size = 100;
			} elseif ( $db_batch_size > 2000 ) {
				$db_batch_size = 2000;
			}

			update_option( Options::backfill_batch_days_key(), $batch_days, false );
			update_option( Options::backfill_delay_seconds_key(), $delay, false );
			update_option( Options::db_batch_size_key(), $db_batch_size, false );

			wp_safe_redirect(
				$this->build_settings_ui_url(
					menu_page_url( 'serpiq-settings', false ),
					array(
						'tab'           => 'sync-import',
						'serpiq_notice' => 'advanced_import_saved',
					)
				)
			);
			exit;
		}

		// Apply a safe preset for shared hosting (small batches, longer pauses).
		if ( isset( $_POST['serpiq_apply_shared_preset'] ) ) {
			check_admin_referer( 'seotune_apply_shared_preset', 'seotune_nonce' );

			// Use conservative defaults for slower servers.
			update_option( Options::backfill_batch_days_key(), 3, false );
			update_option( Options::backfill_delay_seconds_key(), 60, false );
			update_option( Options::db_batch_size_key(), 700, false );

			// Audit log for support.
			try {
				Sync::instance()->log_sync(
					gmdate( 'Y-m-d' ),
					gmdate( 'Y-m-d' ),
					0,
					'success',
					wp_json_encode(
						array(
							'action' => 'apply_shared_hosting_preset',
							'values' => array(
								'batch_days'    => 3,
								'delay_seconds' => 60,
								'db_batch_size' => 700,
							),
						)
					),
					0,
					array(),
					'support_action'
				);
			} catch ( \Throwable $e ) {
				// Best-effort only.
			}

			wp_safe_redirect(
				$this->build_settings_ui_url(
					menu_page_url( 'serpiq-settings', false ),
					array(
						'tab'           => 'sync-import',
						'serpiq_notice' => 'shared_preset_applied',
					)
				)
			);
			exit;
		}

		// Start OAuth flow: WordPress nonce on this admin URL only (anti-CSRF for “start”).
		// The redirect target is Google’s authorize URL, which carries OAuth state — not _wpnonce.
		if ( isset( $_GET['serpiq_oauth_start'] ) ) {
			check_admin_referer( 'seotune_oauth_start', 'seotune_nonce' );

			$url = OAuth::instance()->build_auth_url();
			if ( is_wp_error( $url ) ) {
				wp_safe_redirect( $this->build_settings_ui_url( menu_page_url( 'serpiq-settings', false ), array( 'serpiq_notice' => 'oauth_error' ) ) );
				exit;
			}

			// Google OAuth authorize URL is external; wp_safe_redirect would block without allowed_redirect_hosts.
			// phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- URL built by OAuth::build_auth_url() (Google), not user-controlled.
			wp_redirect( $url );
			exit;
		}

		// OAuth callback: Google returns code + state only (no WordPress nonce on this GET).
		$oauth_callback = $this->get_oauth_callback_get_params();
		if ( null !== $oauth_callback ) {
			global $wpdb;

			$result = OAuth::instance()->handle_oauth_callback(
				sanitize_text_field( $oauth_callback['code'] ),
				sanitize_text_field( $oauth_callback['state'] )
			);

			if ( is_wp_error( $result ) ) {
				wp_safe_redirect( $this->build_settings_ui_url( menu_page_url( 'serpiq-settings', false ), array( 'serpiq_notice' => 'oauth_error' ) ) );
				exit;
			}

			// Detect property right after connection; catch any exception to avoid white screen.
			$redirect_url = menu_page_url( 'serpiq-settings', false );
			try {
				$detected = Property_Detector::instance()->detect_and_store();
				$notice = is_wp_error( $detected ) ? 'property_missing' : 'connected';
				$redirect_url = add_query_arg( 'serpiq_notice', $notice, $redirect_url );

				// On first successful connection and property detection, kick off
				// an initial import of the last 14 days so reports are not empty.
				//
				// This can be disabled globally by defining SERPIQ_DISABLE_AUTO_IMPORT
				// as true in wp-config.php, or by using the serpiq_enable_auto_import filter.
				if ( ! is_wp_error( $detected ) ) {
					$auto_enabled = true;
					if ( defined( 'SERPIQ_DISABLE_AUTO_IMPORT' ) && true === SERPIQ_DISABLE_AUTO_IMPORT ) {
						$auto_enabled = false;
					}
					/**
					 * Filter whether the plugin should trigger the automatic initial import
					 * right after the first successful connection.
					 *
					 * Returning false disables the auto-import.
					 *
					 * @param bool $enabled Default true, unless SERPIQ_DISABLE_AUTO_IMPORT is true.
					 */
					$auto_enabled = (bool) apply_filters( 'serpiq_enable_auto_import', $auto_enabled );

					if ( $auto_enabled ) {
					$pages_table = $wpdb->prefix . 'serpiq_pages';
					$has_table   = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pages_table ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
					$has_data    = false;

					if ( $has_table ) {
						$count = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(1) FROM %i LIMIT 1', $pages_table ) );
						$has_data = $count > 0;
					}

						if ( ! $has_data ) {
							$state  = Backfill::instance()->get_state();
							$status = isset( $state['status'] ) ? (string) $state['status'] : 'idle';

							if ( 'idle' === $status ) {
								try {
									// Initial coverage in background (last 14 days).
									$auto = Backfill::instance()->start( 14 );
									if ( is_wp_error( $auto ) ) {
										set_transient( 'serpiq_auto_backfill_error_msg', sanitize_text_field( $auto->get_error_message() ), 5 * MINUTE_IN_SECONDS );
										$redirect_url = add_query_arg( 'serpiq_notice', 'auto_backfill_error', $redirect_url );
									}
								} catch ( \Throwable $e ) {
									// Do not interrupt the normal redirect flow; diagnostics UI
									// and logs will surface any issues with the auto-import.
									set_transient( 'serpiq_auto_backfill_error_msg', sanitize_text_field( $e->getMessage() ), 5 * MINUTE_IN_SECONDS );
									$redirect_url = add_query_arg( 'serpiq_notice', 'auto_backfill_error', $redirect_url );
								}
							}
						}
					} // end if auto_enabled
				}

				if ( is_wp_error( $detected ) ) {
					set_transient( 'serpiq_property_error_msg', sanitize_text_field( $detected->get_error_message() ), 5 * MINUTE_IN_SECONDS );
				}
			} catch ( \Throwable $e ) {
				set_transient( 'serpiq_property_error_msg', sanitize_text_field( $e->getMessage() ), 5 * MINUTE_IN_SECONDS );
				$redirect_url = add_query_arg( 'serpiq_notice', 'property_missing', $redirect_url );
			}
			wp_safe_redirect( $this->build_settings_ui_url( $redirect_url, array() ) );
			exit;
		}

		// Disconnect.
		if ( isset( $_GET['serpiq_oauth_disconnect'] ) ) {
			check_admin_referer( 'seotune_oauth_disconnect', 'seotune_nonce' );
			OAuth::instance()->disconnect();

			wp_safe_redirect( $this->build_settings_ui_url( menu_page_url( 'serpiq-settings', false ), array( 'serpiq_notice' => 'disconnected' ) ) );
			exit;
		}

		// Start import (backfill) — single path: background only.
		if ( isset( $_POST['serpiq_start_backfill'] ) ) {
			check_admin_referer( 'seotune_start_backfill', 'seotune_nonce' );
			$range_days = isset( $_POST['serpiq_backfill_range'] ) ? absint( wp_unslash( $_POST['serpiq_backfill_range'] ) ) : 0;
			$allowed = array( 30, 90, 365, 485 );
			if ( ! in_array( $range_days, $allowed, true ) ) {
				$range_days = 90;
			}

			$res = Backfill::instance()->start( $range_days );
			$notice = is_wp_error( $res ) ? 'backfill_error' : 'backfill_started';
			wp_safe_redirect(
				$this->build_settings_ui_url(
					menu_page_url( 'serpiq-settings', false ),
					array(
						'tab'           => 'sync-import',
						'serpiq_notice' => $notice,
					)
				)
			);
			exit;
		}

		// Stop/reset historical import (backfill) if stuck.
		if ( isset( $_POST['serpiq_backfill_stop_reset'] ) ) {
			check_admin_referer( 'seotune_backfill_stop_reset', 'seotune_nonce' );
			Backfill::instance()->stop_and_reset();
			wp_safe_redirect( $this->build_settings_ui_url( menu_page_url( 'serpiq-settings', false ), array( 'serpiq_notice' => 'backfill_reset' ) ) );
			exit;
		}

		// Resume historical import from existing coverage (without deleting data).
		if ( isset( $_POST['serpiq_backfill_resume'] ) ) {
			check_admin_referer( 'seotune_backfill_resume', 'seotune_nonce' );
			$res = Backfill::instance()->resume_from_coverage();
			$notice = is_wp_error( $res ) ? 'backfill_resume_error' : 'backfill_resumed';

			// Audit log for support.
			try {
				Sync::instance()->log_sync(
					gmdate( 'Y-m-d' ),
					gmdate( 'Y-m-d' ),
					0,
					is_wp_error( $res ) ? 'error' : 'success',
					wp_json_encode(
						array(
							'action' => 'resume_from_coverage',
							'ok'     => ! is_wp_error( $res ),
							'error'  => is_wp_error( $res ) ? $res->get_error_message() : null,
						)
					),
					0,
					array(),
					'support_action'
				);
			} catch ( \Throwable $e ) {
				// Best-effort only.
			}

			wp_safe_redirect( $this->build_settings_ui_url( menu_page_url( 'serpiq-settings', false ), array( 'serpiq_notice' => $notice ) ) );
			exit;
		}

		// Support tool: force scheduling the next backfill step immediately.
		if ( isset( $_POST['serpiq_backfill_force_next_step'] ) ) {
			check_admin_referer( 'seotune_backfill_force_next_step', 'seotune_nonce' );
			$res = Backfill::instance()->force_next_step();
			$notice = is_wp_error( $res ) ? 'backfill_force_next_step_error' : 'backfill_force_next_step_ok';

			// Audit log for support.
			try {
				Sync::instance()->log_sync(
					gmdate( 'Y-m-d' ),
					gmdate( 'Y-m-d' ),
					0,
					is_wp_error( $res ) ? 'error' : 'success',
					wp_json_encode(
						array(
							'action' => 'force_next_step',
							'ok'     => ! is_wp_error( $res ),
							'error'  => is_wp_error( $res ) ? $res->get_error_message() : null,
						)
					),
					0,
					array(),
					'support_action'
				);
			} catch ( \Throwable $e ) {
				// Best-effort only.
			}

			wp_safe_redirect( $this->build_settings_ui_url( menu_page_url( 'serpiq-settings', false ), array( 'serpiq_notice' => $notice ) ) );
			exit;
		}

		// Web runner (hosting fallback): run a single backfill batch immediately in this request.
		if ( isset( $_POST['serpiq_backfill_run_now'] ) ) {
			check_admin_referer( 'seotune_backfill_run_now', 'seotune_nonce' );
			$before = Backfill::instance()->get_state();
			$before_status = (string) ( $before['status'] ?? 'idle' );
			$notice = 'backfill_run_now_ok';
			$err_msg = null;

			try {
				Backfill::instance()->run_next_batch();
				$after = Backfill::instance()->get_state();
				if ( 'error' === (string) ( $after['status'] ?? '' ) ) {
					$notice = 'backfill_run_now_error';
					$err_msg = (string) ( $after['error_message'] ?? '' );
				} elseif ( 'running' !== $before_status && 'running' === (string) ( $after['status'] ?? '' ) ) {
					// If it wasn't running before, treat as ok but note transition.
					$notice = 'backfill_run_now_ok';
				}
			} catch ( \Throwable $e ) {
				$notice = 'backfill_run_now_error';
				$err_msg = $e->getMessage();
			}

			// Audit log for support.
			try {
				Sync::instance()->log_sync(
					gmdate( 'Y-m-d' ),
					gmdate( 'Y-m-d' ),
					0,
					( 'backfill_run_now_error' === $notice ) ? 'error' : 'success',
					wp_json_encode(
						array(
							'action' => 'run_next_batch_now',
							'ok'     => ( 'backfill_run_now_error' !== $notice ),
							'error'  => $err_msg,
						)
					),
					0,
					array(),
					'support_action'
				);
			} catch ( \Throwable $e ) {
				// Best-effort only.
			}

			$run_now_args = array( 'serpiq_notice' => $notice );
			if ( $err_msg ) {
				set_transient( 'serpiq_run_now_error_msg', sanitize_text_field( $err_msg ), 5 * MINUTE_IN_SECONDS );
			}
			wp_safe_redirect( $this->build_settings_ui_url( menu_page_url( 'serpiq-settings', false ), $run_now_args ) );
			exit;
		}

		// RESET: Delete all imported GSC data and start from scratch.
		if ( isset( $_POST['serpiq_reset_all_gsc_data'] ) ) {
			check_admin_referer( 'seotune_reset_all_gsc_data', 'seotune_nonce' );
			global $wpdb;

			// Stop any running backfill and pending steps.
			Backfill::instance()->stop_and_reset();
			delete_transient( 'serpiq_sync_lock' );

			$tables = array(
				$wpdb->prefix . 'serpiq_queries',
				$wpdb->prefix . 'serpiq_pages',
				$wpdb->prefix . 'serpiq_totals',
				$wpdb->prefix . 'serpiq_alerts',
				$wpdb->prefix . 'serpiq_sync_log',
				$wpdb->prefix . 'serpiq_links',
			);

			$allowed_tables = array(
				$wpdb->prefix . 'serpiq_queries',
				$wpdb->prefix . 'serpiq_pages',
				$wpdb->prefix . 'serpiq_totals',
				$wpdb->prefix . 'serpiq_alerts',
				$wpdb->prefix . 'serpiq_sync_log',
				$wpdb->prefix . 'serpiq_links',
			);

			foreach ( $tables as $table ) {
				if ( ! in_array( $table, $allowed_tables, true ) ) {
					continue;
				}
				if ( ! preg_match( '/^[a-zA-Z0-9_]+$/', $table ) ) {
					continue;
				}
				// TRUNCATE is faster but may be restricted on some hosts; fall back to DELETE.
				$ok = $wpdb->query( $wpdb->prepare( 'TRUNCATE TABLE %i', $table ) );
				if ( false === $ok ) {
					$wpdb->query( $wpdb->prepare( 'DELETE FROM %i', $table ) );
				}
			}

			// Reset sync/backfill markers (keep OAuth connection and settings).
			update_option( Options::last_sync_at_key(), '', false );
			delete_option( Options::backfill_state_key() );

			wp_safe_redirect( $this->build_settings_ui_url( menu_page_url( 'serpiq-settings', false ), array( 'serpiq_notice' => 'reset_done' ) ) );
			exit;
		}
	}

	public function render_app_root(): void {
		if ( ! current_user_can( SERPIQ_CAPABILITY ) ) {
			wp_die( esc_html__( 'You do not have permission to access this page.', 'seotune-search-insights' ) );
		}

		echo '<div class="wrap"><div id="serpiq-admin-app"></div></div>';
	}

	public function render_settings_page(): void {
		if ( ! current_user_can( SERPIQ_CAPABILITY ) ) {
			wp_die( esc_html__( 'You do not have permission to access this page.', 'seotune-search-insights' ) );
		}

		global $wpdb;

		$notice = sanitize_key( $this->get_settings_ui_get_string( 'serpiq_notice' ) );
		$client_id = (string) get_option( Options::google_client_id_key(), '' );
		$client_secret = (string) get_option( Options::google_client_secret_key(), '' );
		$delete_on_uninstall = (int) get_option( Options::delete_data_on_uninstall_key(), 0 );

		$redirect_uri = OAuth::instance()->redirect_uri();
		$connected = OAuth::instance()->has_token();
		$property = (string) get_option( Options::google_property_key(), '' );

		$coverage = Sync::instance()->get_date_coverage();
		$backfill_state = Backfill::instance()->get_state();
		$last_sync_at = (string) get_option( Options::last_sync_at_key(), '' );

		$allowed_tabs = array( 'gsc', 'sync-import', 'about' );
		$tab_raw     = $this->get_settings_ui_get_string( 'tab' );
		$current_tab = $tab_raw !== '' ? sanitize_key( $tab_raw ) : 'gsc';
		if ( ! in_array( $current_tab, $allowed_tabs, true ) ) {
			$current_tab = 'gsc';
		}
		$settings_base_url = menu_page_url( 'serpiq-settings', false );

		echo '<div class="wrap">';
		echo '<div id="serpiq-admin-app"><div class="serpiq-page">';
		echo '<h1 class="serpiq-heading">' . esc_html__( 'Settings', 'seotune-search-insights' ) . '</h1>';

		if ( $notice ) {
			$map = array(
				'saved'                => array( 'success', __( 'Settings saved.', 'seotune-search-insights' ) ),
				'advanced_import_saved'=> array( 'success', __( 'Advanced import settings saved.', 'seotune-search-insights' ) ),
				'shared_preset_applied'=> array( 'success', __( 'Shared hosting preset applied. Historical import settings were updated for slower servers.', 'seotune-search-insights' ) ),
				'connected'            => array( 'success', __( 'Connected to Google. Property detected.', 'seotune-search-insights' ) ),
				'property_missing'     => array( 'warning', __( 'Connected to Google, but this site property was not found in Search Console.', 'seotune-search-insights' ) ),
				'disconnected'         => array( 'success', __( 'Disconnected from Google.', 'seotune-search-insights' ) ),
				'oauth_error'          => array( 'error', __( 'OAuth error. Check credentials and redirect URI.', 'seotune-search-insights' ) ),
				'sync_ok'              => array( 'success', __( 'Sync completed.', 'seotune-search-insights' ) ),
				'sync_error'           => array( 'error', __( 'Sync failed. Check status and logs. Try importing a shorter time range or use the background import.', 'seotune-search-insights' ) ),
				'backfill_started'     => array( 'success', __( 'Historical import started. It will continue in the background. On large sites it may take minutes, hours, or even days to complete.', 'seotune-search-insights' ) ),
				'backfill_error'       => array( 'error', __( 'Historical import could not be started. Check logs.', 'seotune-search-insights' ) ),
				'backfill_reset'       => array( 'success', __( 'Historical import has been stopped and reset.', 'seotune-search-insights' ) ),
				'reset_done'           => array( 'success', __( 'All imported Search Console data has been deleted. You can now sync/import again from scratch.', 'seotune-search-insights' ) ),
				'auto_backfill_error'  => array( 'warning', __( 'Automatic initial import could not be started. You can still start a historical import manually from this page.', 'seotune-search-insights' ) ),
				'backfill_resumed'     => array( 'success', __( 'Historical import has been resumed from existing data; remaining batches will continue from the latest imported date.', 'seotune-search-insights' ) ),
				'backfill_resume_error'=> array( 'error', __( 'Historical import could not be resumed from existing data. Check diagnostics for details.', 'seotune-search-insights' ) ),
				'backfill_force_next_step_ok'   => array( 'success', __( 'Next import step was scheduled to run immediately.', 'seotune-search-insights' ) ),
				'backfill_force_next_step_error'=> array( 'error', __( 'Could not force the next import step. Check diagnostics for details.', 'seotune-search-insights' ) ),
				'backfill_run_now_ok'           => array( 'success', __( 'A backfill batch was run immediately.', 'seotune-search-insights' ) ),
				'backfill_run_now_error'        => array( 'error', __( 'Could not run a backfill batch immediately. See details below.', 'seotune-search-insights' ) ),
			);
			if ( isset( $map[ $notice ] ) ) {
				$type = $map[ $notice ][0];
				$msg  = $map[ $notice ][1];

				$property_error = get_transient( 'serpiq_property_error_msg' );
				if ( $property_error && 'property_missing' === $notice ) {
					delete_transient( 'serpiq_property_error_msg' );
					$msg .= ' ' . __( 'Detail:', 'seotune-search-insights' ) . ' ' . $property_error;
				}

				$auto_error = get_transient( 'serpiq_auto_backfill_error_msg' );
				if ( $auto_error && 'auto_backfill_error' === $notice ) {
					delete_transient( 'serpiq_auto_backfill_error_msg' );
					$msg .= ' ' . __( 'Detail:', 'seotune-search-insights' ) . ' ' . $auto_error;
				}
				// Sync error: recommendation key stored server-side (not in GET).
				$recommendation = '';
				$rec_raw        = '';
				if ( 'sync_error' === $notice ) {
					$rec_stored = get_transient( 'serpiq_sync_error_rec' );
					if ( false !== $rec_stored && is_string( $rec_stored ) && $rec_stored !== '' ) {
						delete_transient( 'serpiq_sync_error_rec' );
						$rec_raw = $rec_stored;
					}
				}
				if ( 'sync_error' === $notice && $rec_raw !== '' ) {
					$rec_key = sanitize_key( $rec_raw );
					$rec_map = array(
						'retry_later'            => __( 'Wait a few minutes and try again, or use Import range (background).', 'seotune-search-insights' ),
						'use_background_import'  => __( 'Use a shorter range (e.g. 3 or 7 days) or use Import range (background).', 'seotune-search-insights' ),
						'use_shorter_range'      => __( 'Use a shorter time range (e.g. 3 or 7 days) or use Import range (background).', 'seotune-search-insights' ),
						'check_connection'       => __( 'Check the connection in the Google Search Console tab, then try a shorter range or use Import range (background).', 'seotune-search-insights' ),
						'check_diagnostics'      => __( 'Check the Import diagnostics section and try again with a shorter range or use Import range (background).', 'seotune-search-insights' ),
						'generic'                => __( 'Try a shorter time range (e.g. 3 or 7 days) or use Import range (background). If the problem persists, check the Import diagnostics section.', 'seotune-search-insights' ),
					);
					if ( isset( $rec_map[ $rec_key ] ) ) {
						$recommendation = $rec_map[ $rec_key ];
					} else {
						$recommendation = $rec_map['generic'];
					}
				}
				$class = 'notice notice-' . $type . ' is-dismissible';
				echo '<div class="' . esc_attr( $class ) . '"><p>' . esc_html( $msg ) . '</p>';
				if ( 'backfill_run_now_error' === $notice ) {
					$run_now_detail = '';
					$stored_err     = get_transient( 'serpiq_run_now_error_msg' );
					if ( false !== $stored_err && is_string( $stored_err ) && $stored_err !== '' ) {
						delete_transient( 'serpiq_run_now_error_msg' );
						$run_now_detail = sanitize_text_field( $stored_err );
					}
					if ( $run_now_detail ) {
						echo '<p style="margin:0.5em 0 0 0;"><strong>' . esc_html__( 'Detail:', 'seotune-search-insights' ) . '</strong> ' . esc_html( $run_now_detail ) . '</p>';
					}
				}
				if ( $recommendation ) {
					echo '<p style="margin:0.5em 0 0 0;"><strong>' . esc_html__( 'Recommended action:', 'seotune-search-insights' ) . '</strong> ' . esc_html( $recommendation ) . '</p>';
				}
				echo '</div>';
			}
		}

		// Tab navigation.
		echo '<nav class="serpiq-settings-tabs-wrap" aria-label="' . esc_attr__( 'Settings sections', 'seotune-search-insights' ) . '">';
		echo '<ul class="serpiq-settings-tabs" data-base-url="' . esc_url( $settings_base_url ) . '" data-settings-ui-nonce="' . esc_attr( wp_create_nonce( self::SETTINGS_UI_NONCE_ACTION ) ) . '">';
		$tabs = array(
			'gsc'          => __( 'Google Search Console', 'seotune-search-insights' ),
			'sync-import'  => __( 'Sync & Import', 'seotune-search-insights' ),
			'about'        => __( 'About SeoTune', 'seotune-search-insights' ),
		);
		foreach ( $tabs as $tab_id => $tab_label ) {
			$url = $this->build_settings_ui_url( $settings_base_url, array( 'tab' => $tab_id ) );
			$active = ( $current_tab === $tab_id ) ? ' serpiq-settings-tab-active' : '';
			echo '<li><a class="' . esc_attr( 'serpiq-settings-tab' . $active ) . '" href="' . esc_url( $url ) . '" data-tab="' . esc_attr( $tab_id ) . '">' . esc_html( $tab_label ) . '</a></li>';
		}
		echo '</ul></nav>';

		echo '<form method="post" action="" id="serpiq-settings-form">';
		wp_nonce_field( 'seotune_settings_save', 'seotune_nonce' );

		// Panel 1: Google Search Console.
		$panel_active = ( 'gsc' === $current_tab ) ? ' serpiq-settings-panel-active' : '';
		echo '<div class="' . esc_attr( 'serpiq-settings-panel' . $panel_active ) . '" data-panel="gsc" id="serpiq-panel-gsc">';
		echo '<div class="serpiq-card" style="border-color: var(--serpiq-accent-light); box-shadow: var(--serpiq-shadow-md);">';
		echo '<h2 class="serpiq-section-title" style="margin-top:0;">' . esc_html__( 'Google Search Console', 'seotune-search-insights' ) . '</h2>';
		echo '<p style="color:var(--serpiq-text-muted); margin-top:0;">' . esc_html__( 'Seotune Search Insights can only use the property matching this site domain. No manual property selection is available.', 'seotune-search-insights' ) . '</p>';
		// Disclaimer about partial data from the Search Console API.
		$gsc_doc_url = 'https://support.google.com/webmasters/answer/96568';
		$disclaimer  = sprintf(
			/* translators: %s: URL to Google Search Console data documentation. */
			__( 'Note: Search Console API data might not include all queries and impressions because Google omits some low-volume or anonymized data to protect user privacy. For details, see <a href="%s" target="_blank" rel="noopener noreferrer">About Search Console data</a>.', 'seotune-search-insights' ),
			esc_url( $gsc_doc_url )
		);
		echo '<p class="description">' . wp_kses(
			$disclaimer,
			array(
				'a' => array(
					'href'   => array(),
					'target' => array(),
					'rel'    => array(),
				),
			)
		) . '</p>';

		echo '<table class="form-table" role="presentation"><tbody>';

		echo '<tr><th scope="row"><label for="serpiq_google_client_id">' . esc_html__( 'Client ID', 'seotune-search-insights' ) . '</label></th><td>';
		echo '<input name="serpiq_google_client_id" id="serpiq_google_client_id" type="text" class="regular-text" value="' . esc_attr( $client_id ) . '"/>';
		echo '</td></tr>';

		echo '<tr><th scope="row"><label for="serpiq_google_client_secret">' . esc_html__( 'Client Secret', 'seotune-search-insights' ) . '</label></th><td>';
		echo '<input name="serpiq_google_client_secret" id="serpiq_google_client_secret" type="password" class="regular-text" value="' . esc_attr( $client_secret ) . '"/>';
		echo '</td></tr>';

		echo '<tr><th scope="row">' . esc_html__( 'Redirect URI', 'seotune-search-insights' ) . '</th><td>';
		echo '<code>' . esc_html( $redirect_uri ) . '</code>';
		echo '</td></tr>';

		echo '<tr><th scope="row">' . esc_html__( 'Detected property', 'seotune-search-insights' ) . '</th><td>';
		echo $property ? '<code>' . esc_html( $property ) . '</code>' : '<em>' . esc_html__( 'Not detected', 'seotune-search-insights' ) . '</em>';
		echo '</td></tr>';

		echo '</tbody></table>';

		echo '<p style="margin: 12px 0 0 0;">';
		if ( $connected ) {
			$disconnect_url = wp_nonce_url( add_query_arg( 'serpiq_oauth_disconnect', '1', menu_page_url( 'serpiq-settings', false ) ), 'seotune_oauth_disconnect', 'seotune_nonce' );
			echo '<a class="serpiq-btn serpiq-btn-secondary" href="' . esc_url( $disconnect_url ) . '">' . esc_html__( 'Disconnect', 'seotune-search-insights' ) . '</a> ';
		} else {
			$connect_url = wp_nonce_url( add_query_arg( 'serpiq_oauth_start', '1', menu_page_url( 'serpiq-settings', false ) ), 'seotune_oauth_start', 'seotune_nonce' );
			echo '<a class="serpiq-btn serpiq-btn-primary" href="' . esc_url( $connect_url ) . '">' . esc_html__( 'Connect to Google', 'seotune-search-insights' ) . '</a> ';
		}
		echo '<button type="submit" form="serpiq-settings-form" name="serpiq_settings_save" value="1" class="serpiq-btn serpiq-btn-primary">' . esc_html__( 'Save Settings', 'seotune-search-insights' ) . '</button>';
		echo '</p>';
		if ( ! $connected ) {
			echo '<p class="description" style="margin: 10px 0 0 0; max-width: 720px;">' . esc_html__( 'The Connect link uses a WordPress admin nonce to start authorization. After you sign in at Google, your browser returns here with an OAuth authorization code and a state value; the plugin verifies state server-side (OAuth anti-CSRF). The WordPress settings-page URL nonce is not sent to Google.', 'seotune-search-insights' ) . '</p>';
		}
		echo '</div>';
		echo '</div>'; // End panel gsc.

		echo '</form>';
		// End of main settings form (no nested forms below).

		// Panel 2: About SeoTune.
		$panel_active = ( 'about' === $current_tab ) ? ' serpiq-settings-panel-active' : '';
		echo '<div class="' . esc_attr( 'serpiq-settings-panel' . $panel_active ) . '" data-panel="about" id="serpiq-panel-about">';
		echo '<div class="serpiq-card">';
		echo '<h2 class="serpiq-section-title" style="margin-top:0;">' . esc_html__( 'About SeoTune', 'seotune-search-insights' ) . '</h2>';
		$seotune_url = 'https://seotune.app/';
		// About tab: use packaged logo asset.
		$seotune_logo_url = SERPIQ_PLUGIN_URL . 'admin/assets/brand/header-logo.png';
		echo '<div style="margin: 10px 0 14px 0;">';
		echo '<a href="' . esc_url( $seotune_url ) . '" target="_blank" rel="noopener noreferrer" aria-label="' . esc_attr__( 'SeoTune website', 'seotune-search-insights' ) . '">';
		echo '<img src="' . esc_url( $seotune_logo_url ) . '" alt="' . esc_attr__( 'SeoTune', 'seotune-search-insights' ) . '" style="height:42px;width:auto;display:block;" />';
		echo '</a>';
		echo '</div>';
		$about_html  = sprintf(
			/* translators: %s: SeoTune website URL. */
			__( '<a href="%s" target="_blank" rel="noopener noreferrer">SeoTune</a> is a project focused on practical, data-driven SEO. This plugin turns Google Search Console data into reports you can actually act on — right inside WordPress.', 'seotune-search-insights' ),
			esc_url( $seotune_url )
		);
		echo '<p style="margin-top:0; max-width: 820px; color:var(--serpiq-text-muted); line-height:1.6;">' . wp_kses(
			$about_html,
			array(
				'a' => array(
					'href'   => array(),
					'target' => array(),
					'rel'    => array(),
				),
			)
		) . '</p>';
		echo '<p class="description" style="margin: -6px 0 0 0; max-width: 820px;">' . esc_html__( 'Note: SeoTune Search Insights is a third-party plugin and is not affiliated with Google.', 'seotune-search-insights' ) . '</p>';

		echo '<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; margin-top: 14px;">';

		echo '<div class="serpiq-card" style="margin:0; background: var(--serpiq-bg-muted);">';
		echo '<h3 style="margin:0 0 6px 0; font-size:14px;">' . esc_html__( 'How to get the most value', 'seotune-search-insights' ) . '</h3>';
		echo '<ul style="margin:0; padding-left: 18px; color:var(--serpiq-text-muted); line-height:1.6;">';
		echo '<li>' . esc_html__( 'Pick a date range and start from Opportunities: improve snippet vs improve content.', 'seotune-search-insights' ) . '</li>';
		echo '<li>' . esc_html__( 'Use Decay to spot pages that are slipping and refresh them before they drop further.', 'seotune-search-insights' ) . '</li>';
		echo '<li>' . esc_html__( 'Export CSVs when you need to work with a team or keep an SEO changelog.', 'seotune-search-insights' ) . '</li>';
		echo '</ul>';
		echo '</div>';

		echo '<div class="serpiq-card" style="margin:0; background: var(--serpiq-bg-muted);">';
		echo '<h3 style="margin:0 0 6px 0; font-size:14px;">' . esc_html__( 'Transparency & privacy', 'seotune-search-insights' ) . '</h3>';
		echo '<ul style="margin:0; padding-left: 18px; color:var(--serpiq-text-muted); line-height:1.6;">';
		echo '<li>' . esc_html__( 'No visitor tracking is added to your site.', 'seotune-search-insights' ) . '</li>';
		echo '<li>' . esc_html__( 'Search Console data is stored in your WordPress database.', 'seotune-search-insights' ) . '</li>';
		echo '<li>' . esc_html__( 'The plugin connects to Google only when an admin authorizes it.', 'seotune-search-insights' ) . '</li>';
		echo '</ul>';
		echo '</div>';

		echo '<div class="serpiq-card" style="margin:0; background: var(--serpiq-bg-muted);">';
		echo '<h3 style="margin:0 0 6px 0; font-size:14px;">' . esc_html__( 'Links', 'seotune-search-insights' ) . '</h3>';
		echo '<p style="margin:0; color:var(--serpiq-text-muted); line-height:1.6;">' . esc_html__( 'Want updates, support, or to share feedback? Add your official links here.', 'seotune-search-insights' ) . '</p>';
		echo '<div style="margin-top:10px; display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:10px; max-width:520px;">';
		echo '<a class="serpiq-btn serpiq-btn-secondary" href="' . esc_url( $seotune_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Website', 'seotune-search-insights' ) . '</a>';
		echo '<a class="serpiq-btn serpiq-btn-secondary" href="' . esc_url( 'https://seotune.app/documentation/' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Documentation', 'seotune-search-insights' ) . '</a>';
		echo '<a class="serpiq-btn serpiq-btn-secondary" href="' . esc_url( 'https://seotune.app/contact-us/' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Support', 'seotune-search-insights' ) . '</a>';
		echo '<a class="serpiq-btn serpiq-btn-secondary" href="' . esc_url( 'https://seotune.app/changelog/' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Changelog', 'seotune-search-insights' ) . '</a>';
		echo '</div>';
		echo '<p class="description" style="margin:10px 0 0 0;">' . esc_html__( 'Tip: replace these placeholders with your real URLs before publishing.', 'seotune-search-insights' ) . '</p>';
		echo '</div>';

		echo '</div>'; // grid
		echo '</div>'; // card
		echo '</div>'; // panel about

		// Panel 3: Sync & Import – single path: import data from Search Console (background only).
		$panel_active = ( 'sync-import' === $current_tab ) ? ' serpiq-settings-panel-active' : '';
		echo '<div class="' . esc_attr( 'serpiq-settings-panel' . $panel_active ) . '" data-panel="sync-import" id="serpiq-panel-sync-import">';
		echo '<div class="serpiq-card">';

		// Import data from Search Console (one section, background only).
		$import_presets = array(
			30  => __( 'Last 30 days', 'seotune-search-insights' ),
			90  => __( 'Last 3 months', 'seotune-search-insights' ),
			365 => __( 'Last 12 months', 'seotune-search-insights' ),
			485 => __( 'Last 16 months', 'seotune-search-insights' ),
		);
		echo '<h3 class="serpiq-section-title" style="margin-top:0; margin-bottom:6px;">' . esc_html__( 'Import data from Search Console', 'seotune-search-insights' ) . '</h3>';
		echo '<p style="margin:0 0 6px 0; font-size:13px; color:var(--serpiq-text-muted);">' . esc_html__( 'Right after the first connection to Google Search Console, Seotune Search Insights automatically imports the last 14 days so that reports are not empty. You can use this section to extend or restart the import for a larger range.', 'seotune-search-insights' ) . '</p>';
		echo '<p style="margin:0 0 14px 0; font-size:13px; color:var(--serpiq-text-muted);">' . esc_html__( 'Choose a range and start the import. It runs in the background — you can leave this page and come back to check progress.', 'seotune-search-insights' ) . '</p>';

		echo '<form method="post" action="" style="max-width:480px; display:flex; align-items:center; gap:10px; flex-wrap:wrap;">';
		wp_nonce_field( 'seotune_start_backfill', 'seotune_nonce' );
		echo '<label for="serpiq_backfill_range" style="font-weight:600;">' . esc_html__( 'Range', 'seotune-search-insights' ) . '</label>';
		echo '<select name="serpiq_backfill_range" id="serpiq_backfill_range">';
		foreach ( $import_presets as $days => $label ) {
			echo '<option value="' . esc_attr( $days ) . '">' . esc_html( $label ) . '</option>';
		}
		echo '</select>';
		submit_button( __( 'Start import', 'seotune-search-insights' ), 'primary', 'serpiq_start_backfill', false );
		echo '<p class="description" style="margin:8px 0 0 0; width:100%;">' . esc_html__( 'After the first import, data is updated automatically every day.', 'seotune-search-insights' ) . '</p>';
		echo '</form>';

		// Advanced settings: let site owners tune how much work is done in each step.
		$backfill_batch_days = (int) get_option( Options::backfill_batch_days_key(), 7 );
		if ( $backfill_batch_days <= 0 ) {
			$backfill_batch_days = 7;
		}

		$backfill_delay_seconds = (int) get_option( Options::backfill_delay_seconds_key(), 60 );
		$delay_allowed          = array( 10, 30, 60 );
		if ( ! in_array( $backfill_delay_seconds, $delay_allowed, true ) ) {
			$backfill_delay_seconds = 60;
		}

		$db_batch_size = (int) get_option( Options::db_batch_size_key(), 1000 );
		if ( $db_batch_size <= 0 ) {
			$db_batch_size = 1000;
		}

		echo '<div class="serpiq-card" style="margin-top:20px; background:var(--serpiq-bg-muted);">';
		echo '<h3 class="serpiq-section-title" style="margin-top:0; margin-bottom:6px;">' . esc_html__( 'Advanced import settings', 'seotune-search-insights' ) . '</h3>';
		echo '<p style="margin:0 0 8px 0; font-size:13px; color:var(--serpiq-text-muted);">' . esc_html__( 'These options control how much work is done in each step of the historical import. The default values work well for most sites.', 'seotune-search-insights' ) . '</p>';
		echo '<p style="margin:0 0 6px 0; font-size:12px; color:var(--serpiq-text-muted);">' . esc_html__( 'If your site runs on shared or slow hosting, you can apply a preset with smaller, safer batches.', 'seotune-search-insights' ) . '</p>';
		echo '<form method="post" action="" style="margin:0 0 10px 0; display:inline-flex; align-items:center; gap:8px; flex-wrap:wrap;">';
		wp_nonce_field( 'seotune_apply_shared_preset', 'seotune_nonce' );
		echo '<button type="submit" class="serpiq-btn serpiq-btn-secondary" name="serpiq_apply_shared_preset" value="1">' . esc_html__( 'Apply shared hosting preset', 'seotune-search-insights' ) . '</button>';
		echo '<span style="font-size:11px; color:var(--serpiq-text-muted);">' . esc_html__( 'Uses 3 days per block, 60s pause, and ~700 rows per DB batch.', 'seotune-search-insights' ) . '</span>';
		echo '</form>';
		echo '<p style="margin:0 0 10px 0; font-size:12px; color:var(--serpiq-text-muted);">' . esc_html__( 'For faster and more reliable imports on low-traffic or shared hosting, you can also configure a real server cron to call WordPress cron directly.', 'seotune-search-insights' ) . '</p>';

		// Days per batch.
		echo '<div style="margin-top:10px; margin-bottom:10px;">';
		echo '<p style="margin:0 0 4px 0; font-size:13px; color:var(--serpiq-text-muted);"><strong>' . esc_html__( 'Days per import block', 'seotune-search-insights' ) . '</strong></p>';
		echo '<p style="margin:0 0 6px 0; font-size:12px; color:var(--serpiq-text-muted);">' . esc_html__( 'How many days of data are imported at once in each step. Smaller blocks reduce the load on slow hosting; larger blocks finish faster on stronger servers.', 'seotune-search-insights' ) . '</p>';
		echo '<form method="post" action="" style="display:flex; flex-direction:column; gap:10px; margin:0;">';
		wp_nonce_field( 'seotune_save_advanced_import_settings', 'seotune_nonce' );
		echo '<label for="serpiq_backfill_batch_days" style="font-size:13px; color:var(--serpiq-text-muted);">' . esc_html__( 'Days per block', 'seotune-search-insights' ) . '</label>';
		echo '<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">';
		echo '<input type="number" min="1" max="30" step="1" name="serpiq_backfill_batch_days" id="serpiq_backfill_batch_days" class="serpiq-input" style="width:80px;" value="' . esc_attr( $backfill_batch_days ) . '"/>';
		echo '</div>';
		echo '</div>';

		// Delay between batches.
		echo '<div style="margin-top:6px; margin-bottom:10px;">';
		echo '<p style="margin:0 0 4px 0; font-size:13px; color:var(--serpiq-text-muted);"><strong>' . esc_html__( 'Pause between import blocks', 'seotune-search-insights' ) . '</strong></p>';
		echo '<p style="margin:0 0 6px 0; font-size:12px; color:var(--serpiq-text-muted);">' . esc_html__( 'Time to wait (in seconds) between one block and the next. Longer pauses reduce server load but make the import take more time.', 'seotune-search-insights' ) . '</p>';
		echo '<label for="serpiq_backfill_delay_seconds" style="font-size:13px; color:var(--serpiq-text-muted);">' . esc_html__( 'Pause between blocks', 'seotune-search-insights' ) . '</label>';
		echo '<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">';
		echo '<select name="serpiq_backfill_delay_seconds" id="serpiq_backfill_delay_seconds" class="serpiq-select" style="width:110px;">';
		foreach ( $delay_allowed as $sec ) {
			/* translators: %d: pause duration in seconds between import blocks (10, 30, or 60). */
			$delay_label = sprintf( _n( '%d second', '%d seconds', $sec, 'seotune-search-insights' ), $sec );
			echo '<option value="' . esc_attr( $sec ) . '"' . selected( $backfill_delay_seconds, $sec, false ) . '>' . esc_html( $delay_label ) . '</option>';
		}
		echo '</select>';
		echo '</div>';
		echo '</div>';

		// Rows per DB batch.
		echo '<div style="margin-top:6px; margin-bottom:10px;">';
		echo '<p style="margin:0 0 4px 0; font-size:13px; color:var(--serpiq-text-muted);"><strong>' . esc_html__( 'Rows saved per database batch', 'seotune-search-insights' ) . '</strong></p>';
		echo '<p style="margin:0 0 6px 0; font-size:12px; color:var(--serpiq-text-muted);">' . esc_html__( 'How many result rows are written to the database in one go. Higher values can speed up imports but use more memory and database resources. For shared hosting, 500–1000 is usually safe.', 'seotune-search-insights' ) . '</p>';
		echo '<label for="serpiq_db_batch_size" style="font-size:13px; color:var(--serpiq-text-muted);">' . esc_html__( 'Rows per batch', 'seotune-search-insights' ) . '</label>';
		echo '<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">';
		echo '<input type="number" min="100" max="2000" step="50" name="serpiq_db_batch_size" id="serpiq_db_batch_size" class="serpiq-input" style="width:100px;" value="' . esc_attr( $db_batch_size ) . '"/>';
		echo '</div>';
		echo '</div>';

		echo '<div style="display:flex; justify-content:flex-end; margin-top:10px;">';
		echo '<button type="submit" class="serpiq-btn serpiq-btn-primary" name="serpiq_advanced_import_save" value="1">' . esc_html__( 'Save', 'seotune-search-insights' ) . '</button>';
		echo '</div>';
		echo '</form>';

		// Server-side cron example.
		echo '<div style="margin-top:4px; border-top:1px solid rgba(15,23,42,0.08); padding-top:8px; font-size:12px; color:var(--serpiq-text-muted);">';
		echo '<p style="margin:0 0 4px 0;"><strong>' . esc_html__( 'Optional: server-side cron for wp-cron', 'seotune-search-insights' ) . '</strong></p>';
		echo '<p style="margin:0 0 4px 0;">' . esc_html__( 'On low-traffic sites, WordPress cron may not run often enough to complete the historical import quickly. You can configure a real server cron job to call wp-cron.php every few minutes.', 'seotune-search-insights' ) . '</p>';
		echo '<p style="margin:0 0 2px 0;">' . esc_html__( 'Example cron entry (runs wp-cron.php every 5 minutes):', 'seotune-search-insights' ) . '</p>';
		echo '<code style="display:inline-block; padding:4px 6px; background:#0f172a0d; border-radius:4px; font-size:11px;">*/5 * * * * /usr/bin/php /path/to/wordpress/wp-cron.php &gt; /dev/null 2&gt;&amp;1</code>';
		echo '</div>';

		// Milestone 2.2: alternative runner for restrictive hosting (WP-CLI).
		echo '<div style="margin-top:10px; border-top:1px solid rgba(15,23,42,0.08); padding-top:8px; font-size:12px; color:var(--serpiq-text-muted);">';
		echo '<p style="margin:0 0 4px 0;"><strong>' . esc_html__( 'Recommended for restrictive hosting: WP-CLI runner', 'seotune-search-insights' ) . '</strong></p>';
		echo '<p style="margin:0 0 6px 0;">' . esc_html__( 'If your hosting blocks or delays WP-Cron, you can run the historical import from the server using WP-CLI. This does not replace WP-Cron, but provides a robust “pro” path for completing imports reliably.', 'seotune-search-insights' ) . '</p>';
		echo '<p style="margin:0 0 4px 0;">' . esc_html__( 'Manual run (runs one due batch):', 'seotune-search-insights' ) . '</p>';
		echo '<code style="display:inline-block; padding:4px 6px; background:#0f172a0d; border-radius:4px; font-size:11px;">wp serpiq backfill run --due-now</code>';
		echo '<p style="margin:6px 0 4px 0;">' . esc_html__( 'Example server cron (every 2 minutes, runs at most 1 batch):', 'seotune-search-insights' ) . '</p>';
		echo '<code style="display:inline-block; padding:4px 6px; background:#0f172a0d; border-radius:4px; font-size:11px;">*/2 * * * * cd /path/to/wordpress &amp;&amp; wp serpiq backfill run --due-now --max=1 --quiet</code>';
		echo '</div>';

		echo '</div>'; // End advanced import settings card.

		// Import status (single block: last import, coverage, and running/error/idle state).
		$earliest = $coverage['earliest'] ?? null;
		$latest   = $coverage['latest'] ?? null;

		// Always compute "real" progress from stored data (not from state counters),
		// so the UI never stays at 0% when data is already present.
		$coverage_progress = Backfill::instance()->get_current_import_coverage();
		$covered_days      = isset( $coverage_progress['coveredDays'] ) ? (int) $coverage_progress['coveredDays'] : 0;
		$covered_total     = isset( $coverage_progress['totalDays'] ) ? (int) $coverage_progress['totalDays'] : 0;
		$covered_percent   = ( $covered_total > 0 ) ? (int) round( ( $covered_days / $covered_total ) * 100 ) : 0;

		// Also compute overall data coverage days (global tables) for idle/completed states.
		$overall_days = 0;
		if ( $earliest && $latest ) {
			$start_ts = strtotime( (string) $earliest );
			$end_ts   = strtotime( (string) $latest );
			if ( false !== $start_ts && false !== $end_ts && $start_ts <= $end_ts ) {
				$overall_days = (int) floor( ( $end_ts - $start_ts ) / DAY_IN_SECONDS ) + 1;
			}
		}

		// If the stored backfill_state got out of sync with the real data coverage
		// (for example on some shared hosting with aggressive caching), try to
		// resync imported_days/current_date from the tables so the progress
		// indicator reflects what is actually in the database.
		$state_resynced = false;
		if ( in_array( $backfill_state['status'] ?? 'idle', array( 'running', 'error' ), true ) ) {
			$covers_start      = ! empty( $coverage_progress['coversStart'] );
			$covers_end        = ! empty( $coverage_progress['coversEnd'] );
			$total_days_state  = isset( $backfill_state['total_days'] ) ? (int) $backfill_state['total_days'] : 0;
			$total_days_cov    = $covered_total;
			$target_total      = $total_days_state > 0 ? $total_days_state : $total_days_cov;

			if ( $covered_days > 0 && $target_total > 0 ) {
				$current_imported = isset( $backfill_state['imported_days'] ) ? (int) $backfill_state['imported_days'] : 0;
				$new_imported     = min( $covered_days, $target_total );

				// Only resync when coverage is clearly ahead of the stored state.
				if ( $new_imported > $current_imported ) {
					$backfill_state['imported_days'] = $new_imported;
					// Only advance cursor when coverage starts from the target start.
					if ( $covers_start && ! empty( $coverage_progress['latest'] ) ) {
						$backfill_state['current_date'] = (string) $coverage_progress['latest'];
					}

					// Mark completed only when the data covers the full target range.
					if ( $covers_start && $covers_end && $covered_days >= $target_total ) {
						$backfill_state['status']       = 'completed';
						$backfill_state['completed_at'] = $backfill_state['completed_at'] ?? gmdate( 'c' );
					}

					update_option( Options::backfill_state_key(), $backfill_state, false );
					$state_resynced = true;
				}
			}
		}

		$status = $backfill_state['status'] ?? 'idle';
		echo '<h3 class="serpiq-section-title" style="margin-top:24px; margin-bottom:6px;">' . esc_html__( 'Import status', 'seotune-search-insights' ) . '</h3>';
		if ( 'running' === $status ) {
			$total_days = max( 1, (int) $backfill_state['total_days'] );
			$imported   = min( $total_days, (int) $backfill_state['imported_days'] );
			$percent    = (int) round( ( $imported / $total_days ) * 100 );
			// Ensure target totals are usable even if coverage helper can't compute totals yet.
			if ( $covered_total <= 0 ) {
				$covered_total = $total_days;
				$covered_percent = ( $covered_total > 0 ) ? (int) round( ( $covered_days / $covered_total ) * 100 ) : 0;
			}

			$stuck = Backfill::instance()->is_stuck( $backfill_state );
			echo '<div class="notice notice-info" style="margin-top:16px;">';
			echo '<p style="display:flex; align-items:center; gap:8px; margin:0 0 6px 0;">';
			// Simple CSS spinner (uses current text color).
			echo '<span class="serpiq-spinner" style="display:inline-block; width:14px; height:14px; border-radius:999px; border:2px solid rgba(15,118,110,0.25); border-top-color:rgba(15,118,110,1); animation:serpiq-spin 0.8s linear infinite;"></span>';
			echo '<strong>' . esc_html__( 'Import in progress.', 'seotune-search-insights' ) . '</strong>';
			echo '</p>';
			$target_total = max( 1, (int) $total_days );
			$msg = sprintf(
				/* translators: %d: number of days in the import range. */
				_n(
					'Seotune Search Insights is importing the last %d day. You can stay on this page or come back later: the operation continues in the background.',
					'Seotune Search Insights is importing the last %d days. You can stay on this page or come back later: the operation continues in the background.',
					$target_total,
					'seotune-search-insights'
				),
				$target_total
			);
			echo '<p style="margin:0 0 8px 0; font-size:12px; background:#e7f3ff; color:#0b4f8a; padding:6px 10px; border-radius:4px; display:inline-block;">';
			echo esc_html( $msg );
			echo '</p>';
			echo '<p style="margin:6px 0 0 0;">';
			echo esc_html__( 'Target range:', 'seotune-search-insights' ) . ' ';
			echo '<code>' . esc_html( $backfill_state['target_start_date'] ?? '' ) . '</code> &rarr; <code>' . esc_html( $backfill_state['target_end_date'] ?? '' ) . '</code><br/>';
			echo esc_html__( 'Imported days (planned):', 'seotune-search-insights' ) . ' ' . esc_html( "{$imported} / {$total_days} ({$percent}%)" );
			echo '<br/>' . esc_html__( 'Imported days (from data):', 'seotune-search-insights' ) . ' ' . esc_html( "{$covered_days} / {$covered_total} ({$covered_percent}%)" );
			if ( ! empty( $coverage_progress['earliest'] ) && ! empty( $coverage_progress['latest'] ) ) {
				echo '<br/>' . esc_html__( 'Data coverage in this import:', 'seotune-search-insights' ) . ' <code>' . esc_html( $coverage_progress['earliest'] ) . '</code> &rarr; <code>' . esc_html( $coverage_progress['latest'] ) . '</code>';
			}
			if ( ! empty( $backfill_state['last_batch_start'] ) && ! empty( $backfill_state['last_batch_end'] ) ) {
				$rows = (int) ( $backfill_state['last_batch_rows'] ?? 0 );
				$dur  = (int) ( $backfill_state['last_batch_duration'] ?? 0 );
				echo '<br/>' . esc_html__( 'Last batch:', 'seotune-search-insights' ) . ' ';
				echo '<code>' . esc_html( $backfill_state['last_batch_start'] ) . '</code> &rarr; <code>' . esc_html( $backfill_state['last_batch_end'] ) . '</code>';
				/* translators: %d: number of database rows imported in the last batch. */
				$rows_label = sprintf( _n( '%d row', '%d rows', $rows, 'seotune-search-insights' ), $rows );
				echo ' — ' . esc_html( $rows_label ) . ', ' . esc_html( (string) $dur ) . ' ms';
			}
			echo '</p>';
			if ( $state_resynced ) {
				echo '<p style="margin:4px 0 0 0; font-size:11px; color:var(--serpiq-text-muted);">' . esc_html__( 'Import state was resynced from existing data to match the coverage already stored in the database.', 'seotune-search-insights' ) . '</p>';
			}
			echo '<form method="post" action="" style="margin-top:12px; margin-bottom:12px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">';
			wp_nonce_field( 'seotune_backfill_stop_reset', 'seotune_nonce' );
			echo '<input type="hidden" name="serpiq_backfill_stop_reset" value="1"/>';
			echo '<button type="submit" class="serpiq-btn serpiq-btn-secondary"';
			echo $stuck ? '' : ' onclick="return confirm(\'Stop and reset the historical import? This will clear its progress and you can start again.\');"';
			echo '>';
			echo esc_html__( 'Stop / Reset import', 'seotune-search-insights' );
			echo '</button>';
			// Manual refresh button: reloads the Sync & Import tab so progress is updated.
			$refresh_url = $this->build_settings_ui_url( $settings_base_url, array( 'tab' => 'sync-import' ) );
			echo '<button type="button" class="serpiq-btn serpiq-btn-secondary" onclick="window.location.href=\'' . esc_url( $refresh_url ) . '\';" title="' . esc_attr__( 'Reload this page to see the latest progress.', 'seotune-search-insights' ) . '">';
			echo esc_html__( 'Refresh progress', 'seotune-search-insights' );
			echo '</button>';
			// Hosting fallback: if no batches have run yet, allow running one batch immediately (no WP-Cron required).
			$created_ts = ! empty( $backfill_state['created_at'] ) ? strtotime( (string) $backfill_state['created_at'] ) : false;
			$no_batches_yet = empty( $backfill_state['last_batch_start'] ) && empty( $backfill_state['last_batch_end'] );
			$waited_long_enough = $created_ts ? ( time() - (int) $created_ts ) > 180 : false;
			if ( $no_batches_yet && $waited_long_enough ) {
				echo '</form>';
				echo '<form method="post" action="" style="margin-top:8px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">';
				wp_nonce_field( 'seotune_backfill_run_now', 'seotune_nonce' );
				echo '<input type="hidden" name="serpiq_backfill_run_now" value="1"/>';
				echo '<button type="submit" class="serpiq-btn serpiq-btn-secondary" title="' . esc_attr__( 'Runs one backfill batch immediately in this request (useful on restrictive hosting).', 'seotune-search-insights' ) . '">';
				echo esc_html__( 'Run one batch now', 'seotune-search-insights' );
				echo '</button>';
				echo '<p style="margin:0; font-size:12px; color:var(--serpiq-text-muted);">' . esc_html__( 'If this works but cron does not, configure server-side cron or WP-CLI runner.', 'seotune-search-insights' ) . '</p>';
				echo '</form>';
				echo '<form method="post" action="" style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:8px;">';
				wp_nonce_field( 'seotune_backfill_stop_reset', 'seotune_nonce' );
				echo '<input type="hidden" name="serpiq_backfill_stop_reset" value="1"/>';
			}
			if ( $stuck ) {
				echo '<p style="margin:8px 0 0 0; color: var(--serpiq-text-muted); font-size: 12px;">' . esc_html__( 'It looks like the import is not progressing. Use Stop / Reset to clear the state and start again.', 'seotune-search-insights' ) . '</p>';
			} else {
				echo '<p style="margin:8px 0 0 0; color: var(--serpiq-text-muted); font-size: 12px;">' . esc_html__( 'Click “Refresh progress” to update the Imported days counter without restarting the import.', 'seotune-search-insights' ) . '</p>';
			}
			echo '</form>';
			if ( $stuck ) {
				// Resume and force-next-step need their own nonces (separate forms).
				echo '<form method="post" action="" style="margin-top:8px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">';
				wp_nonce_field( 'seotune_backfill_resume', 'seotune_nonce' );
				echo '<input type="hidden" name="serpiq_backfill_resume" value="1"/>';
				echo '<button type="submit" class="serpiq-btn serpiq-btn-secondary" style="border-color:rgba(234,179,8,0.55);">';
				echo esc_html__( 'Resume from current data', 'seotune-search-insights' );
				echo '</button>';
				echo '</form>';

				echo '<form method="post" action="" style="margin-top:8px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">';
				wp_nonce_field( 'seotune_backfill_force_next_step', 'seotune_nonce' );
				echo '<input type="hidden" name="serpiq_backfill_force_next_step" value="1"/>';
				echo '<button type="submit" class="serpiq-btn serpiq-btn-secondary" title="' . esc_attr__( 'Support tool: schedule the next backfill step to run immediately.', 'seotune-search-insights' ) . '">';
				echo esc_html__( 'Force next step', 'seotune-search-insights' );
				echo '</button>';
				echo '</form>';
			}
			echo '</div>';
		} elseif ( 'error' === $status ) {
			echo '<div class="notice notice-error" style="margin-top:16px;">';
			echo '<p><strong>' . esc_html__( 'Import error.', 'seotune-search-insights' ) . '</strong> ';
			if ( ! empty( $backfill_state['error_message'] ) ) {
				echo esc_html( $backfill_state['error_message'] );
			}
			echo '</p>';
			if ( $covered_total > 0 ) {
				echo '<p style="margin:6px 0 0 0;">';
				echo esc_html__( 'Imported days (from data):', 'seotune-search-insights' ) . ' ' . esc_html( "{$covered_days} / {$covered_total} ({$covered_percent}%)" );
				if ( ! empty( $coverage_progress['earliest'] ) && ! empty( $coverage_progress['latest'] ) ) {
					echo '<br/>' . esc_html__( 'Data coverage in this import:', 'seotune-search-insights' ) . ' <code>' . esc_html( $coverage_progress['earliest'] ) . '</code> &rarr; <code>' . esc_html( $coverage_progress['latest'] ) . '</code>';
				}
				echo '</p>';
			}
			echo '</div>';

			// Allow user to resume from existing data or clear the broken import state.
			echo '<form method="post" action="" style="margin-top:12px; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">';
			wp_nonce_field( 'seotune_backfill_resume', 'seotune_nonce' );
			echo '<input type="hidden" name="serpiq_backfill_resume" value="1"/>';
			echo '<button type="submit" class="serpiq-btn serpiq-btn-secondary">';
			echo esc_html__( 'Resume import from current data', 'seotune-search-insights' );
			echo '</button>';
			echo '</form>';

			echo '<form method="post" action="" style="margin-top:8px;">';
			wp_nonce_field( 'seotune_backfill_stop_reset', 'seotune_nonce' );
			echo '<input type="hidden" name="serpiq_backfill_stop_reset" value="1"/>';
			echo '<button type="submit" class="serpiq-btn serpiq-btn-secondary"';
			echo ' onclick="return confirm(\'Stop and reset the historical import state? This will clear its progress and you can start a new import.\');"';
			echo '>';
			echo esc_html__( 'Reset import state', 'seotune-search-insights' );
			echo '</button>';
			echo '</form>';
		} elseif ( 'completed' === $status && ! empty( $backfill_state['completed_at'] ) ) {
			echo '<div class="notice notice-success" style="margin-top:16px;">';
			echo '<p><strong>' . esc_html__( 'Import completed.', 'seotune-search-insights' ) . '</strong> ';
			/* translators: %s: import completion timestamp (ISO 8601), shown in UTC. */
			echo esc_html( sprintf( __( 'Finished at %s (UTC).', 'seotune-search-insights' ), $backfill_state['completed_at'] ) );
			echo '</p>';
			if ( $covered_total > 0 ) {
				echo '<p style="margin:6px 0 0 0;">';
				echo esc_html__( 'Imported days (from data):', 'seotune-search-insights' ) . ' ' . esc_html( "{$covered_days} / {$covered_total} ({$covered_percent}%)" );
				if ( ! empty( $coverage_progress['earliest'] ) && ! empty( $coverage_progress['latest'] ) ) {
					echo '<br/>' . esc_html__( 'Data coverage in this import:', 'seotune-search-insights' ) . ' <code>' . esc_html( $coverage_progress['earliest'] ) . '</code> &rarr; <code>' . esc_html( $coverage_progress['latest'] ) . '</code>';
				}
				echo '</p>';
			}
			echo '</div>';
			if ( '' !== $last_sync_at ) {
				echo '<p style="margin:8px 0 0 0; font-size:13px; color:var(--serpiq-text-muted);">' . esc_html__( 'Last import:', 'seotune-search-insights' ) . ' <code>' . esc_html( $last_sync_at ) . '</code></p>';
			}
			if ( $earliest && $latest ) {
				echo '<p style="margin:4px 0 0 0; font-size:13px; color:var(--serpiq-text-muted);">' . esc_html__( 'Data coverage:', 'seotune-search-insights' ) . ' <code>' . esc_html( $earliest ) . '</code> &rarr; <code>' . esc_html( $latest ) . '</code></p>';
			}
		} else {
			// Idle: show no data or last import + coverage.
			if ( ! $earliest && ! $latest ) {
				echo '<p style="margin-top:12px; font-size:13px; color:var(--serpiq-text-muted);">' . esc_html__( 'No data yet. Start an import above.', 'seotune-search-insights' ) . '</p>';
			} else {
				echo '<p style="margin-top:12px; font-size:13px; color:var(--serpiq-text-muted);">';
				if ( '' !== $last_sync_at ) {
					echo esc_html__( 'Last import:', 'seotune-search-insights' ) . ' <code>' . esc_html( $last_sync_at ) . '</code>';
				} else {
					echo esc_html__( 'Last import:', 'seotune-search-insights' ) . ' <em>' . esc_html__( 'Not available yet', 'seotune-search-insights' ) . '</em>';
				}
				echo '</p>';
				if ( $overall_days > 0 ) {
					echo '<p style="margin:4px 0 0 0; font-size:13px; color:var(--serpiq-text-muted);">' . esc_html__( 'Imported days (from data):', 'seotune-search-insights' ) . ' <strong>' . esc_html( (string) $overall_days ) . '</strong></p>';
				}
				if ( $earliest && $latest ) {
					echo '<p style="margin:4px 0 0 0; font-size:13px; color:var(--serpiq-text-muted);">' . esc_html__( 'Data coverage:', 'seotune-search-insights' ) . ' <code>' . esc_html( $earliest ) . '</code> &rarr; <code>' . esc_html( $latest ) . '</code></p>';
				}
			}
		}

		// Import diagnostics: show when status is error or user requested it.
		$show_diagnostics = ( 'error' === $status ) || ( '1' === $this->get_settings_ui_get_string( 'serpiq_show_diagnostics' ) );
		if ( ! $show_diagnostics ) {
			$diag_url = $this->build_settings_ui_url(
				$settings_base_url,
				array(
					'tab'                     => 'sync-import',
					'serpiq_show_diagnostics' => '1',
				)
			);
			echo '<p style="margin-top:16px; font-size:13px;">';
			echo '<a href="' . esc_url( $diag_url ) . '">' . esc_html__( 'Troubleshoot (show diagnostics)', 'seotune-search-insights' ) . '</a>';
			echo '</p>';
		} else {
		$diag = Backfill::instance()->get_cron_diagnostics();
		$backfill_full_state = Backfill::instance()->get_state();
		$health = isset( $backfill_full_state['last_health_check'] ) && is_array( $backfill_full_state['last_health_check'] )
			? $backfill_full_state['last_health_check']
			: null;
		// Ensure a health check snapshot exists when diagnostics are shown,
		// so runtime diagnostics and support bundles are always populated.
		if ( ! is_array( $health ) ) {
			try {
				$health = Backfill::instance()->run_health_check();
				$backfill_full_state = Backfill::instance()->get_state();
			} catch ( \Throwable $e ) {
				$health = null;
			}
		}
		$level = isset( $diag['level'] ) ? (string) $diag['level'] : 'ok';
		$code  = isset( $diag['code'] ) ? (string) $diag['code'] : '';
		$message = isset( $diag['message'] ) ? (string) $diag['message'] : '';
		$hint    = isset( $diag['hint'] ) ? (string) $diag['hint'] : '';
		$last_log_at = isset( $diag['last_log_at'] ) ? (string) $diag['last_log_at'] : '';
		$last_backfill_at = isset( $diag['last_backfill_log_at'] ) ? (string) $diag['last_backfill_log_at'] : '';

		$badge_bg = 'var(--serpiq-bg-muted)';
		if ( 'error' === $level ) {
			$badge_bg = 'rgba(220, 38, 38, 0.12)';
		} elseif ( 'warning' === $level ) {
			$badge_bg = 'rgba(234, 179, 8, 0.12)';
		}

		echo '<div class="serpiq-card" style="margin-top:16px;">';
		echo '<h3 class="serpiq-section-title" style="margin-top:0;">' . esc_html__( 'Import diagnostics', 'seotune-search-insights' ) . '</h3>';

		// Health check details (if available).
		$health_raw_lines = array();
		if ( is_array( $health ) ) {
			// Runtime requirements / environment checks (Milestone: self-evaluating diagnostics).
			$parse_shorthand = static function ( $val ): int {
				$val = is_string( $val ) ? trim( $val ) : (string) $val;
				if ( '' === $val ) {
					return 0;
				}
				$last = strtolower( substr( $val, -1 ) );
				$num  = (int) $val;
				if ( 'g' === $last ) {
					return $num * 1024 * 1024 * 1024;
				}
				if ( 'm' === $last ) {
					return $num * 1024 * 1024;
				}
				if ( 'k' === $last ) {
					return $num * 1024;
				}
				return (int) $val;
			};

			$wp_ok  = version_compare( get_bloginfo( 'version' ), '6.0', '>=' );
			$php_ok = version_compare( PHP_VERSION, '8.2', '>=' );

			$max_exec = (int) ini_get( 'max_execution_time' );
			$max_exec_level = ( $max_exec >= 120 || 0 === $max_exec ) ? 'ok' : ( $max_exec >= 60 ? 'warning' : 'error' );

			$mem_raw = (string) ini_get( 'memory_limit' );
			$mem_b   = $parse_shorthand( $mem_raw );
			$mem_level = ( $mem_b >= 256 * 1024 * 1024 || 0 === $mem_b ) ? 'ok' : ( $mem_b >= 128 * 1024 * 1024 ? 'warning' : 'error' );

			$disable_wp_cron = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
			$backfill_next_ts = wp_next_scheduled( 'serpiq_backfill_step' );
			$daily_next_ts    = wp_next_scheduled( 'serpiq_daily_sync' );
			$cron_late_backfill = $backfill_next_ts && ( (int) $backfill_next_ts < ( time() - 60 ) );
			$cron_late_daily    = $daily_next_ts && ( (int) $daily_next_ts < ( time() - 60 ) );

			// Traffic/cron triggering proxy: if no logs in the last 6h, cron likely not firing regularly.
			$last_any_log = isset( $health['logs']['last_log_created_at'] ) ? (string) $health['logs']['last_log_created_at'] : '';
			$last_any_ts  = $last_any_log ? strtotime( $last_any_log ) : false;
			$recent_logs_ok = $last_any_ts && ( time() - (int) $last_any_ts ) < 6 * HOUR_IN_SECONDS;

			// Loopback request check (helps detect WAF/security layers blocking internal HTTP).
			$loopback_ok = null;
			$loopback_note = '';
			try {
				$resp = wp_remote_get(
					add_query_arg( 'serpiq_probe', '1', home_url( '/wp-cron.php' ) ),
					array(
						'timeout'     => 5,
						'redirection' => 2,
						'headers'     => array( 'Cache-Control' => 'no-cache' ),
					)
				);
				if ( is_wp_error( $resp ) ) {
					$loopback_ok = false;
					$loopback_note = $resp->get_error_message();
				} else {
					$code = (int) wp_remote_retrieve_response_code( $resp );
					$loopback_ok = ( $code >= 200 && $code < 500 );
					$loopback_note = 'HTTP ' . $code;
				}
			} catch ( \Throwable $e ) {
				$loopback_ok = false;
				$loopback_note = $e->getMessage();
			}

			// Time consistency: compare server time() vs WP UTC timestamp.
			$wp_utc_ts = (int) current_time( 'timestamp', true );
			$time_drift = abs( time() - $wp_utc_ts );
			$time_ok = $time_drift < 120;

			echo '<div style="margin:10px 0 10px 0; padding:10px; border:1px solid rgba(15,23,42,0.08); border-radius:8px; background:var(--serpiq-bg-muted);">';
			echo '<p style="margin:0 0 8px 0; font-size:12px; font-weight:600;">' . esc_html__( 'Runtime diagnostics', 'seotune-search-insights' ) . '</p>';
			echo '<table class="widefat striped" style="margin:0; font-size:12px;">';
			echo '<thead><tr>';
			echo '<th>' . esc_html__( 'Check', 'seotune-search-insights' ) . '</th>';
			echo '<th>' . esc_html__( 'Result', 'seotune-search-insights' ) . '</th>';
			echo '<th>' . esc_html__( 'Recommended action', 'seotune-search-insights' ) . '</th>';
			echo '</tr></thead><tbody>';

			// WordPress version.
			echo '<tr><td>' . esc_html__( 'WordPress version (6.x+)', 'seotune-search-insights' ) . '</td><td><span style="' . esc_attr( $this->diagnostic_badge_style_attr( $wp_ok ? 'ok' : 'error' ) ) . '">' . esc_html( $wp_ok ? 'OK' : 'FAIL' ) . '</span> <code>' . esc_html( get_bloginfo( 'version' ) ) . '</code></td><td>' . esc_html( $wp_ok ? __( 'No action needed.', 'seotune-search-insights' ) : __( 'Update WordPress to a supported version.', 'seotune-search-insights' ) ) . '</td></tr>';

			// PHP version.
			echo '<tr><td>' . esc_html__( 'PHP version (8.2+ recommended)', 'seotune-search-insights' ) . '</td><td><span style="' . esc_attr( $this->diagnostic_badge_style_attr( $php_ok ? 'ok' : 'warning' ) ) . '">' . esc_html( $php_ok ? 'OK' : 'WARN' ) . '</span> <code>' . esc_html( PHP_VERSION ) . '</code></td><td>' . esc_html( $php_ok ? __( 'No action needed.', 'seotune-search-insights' ) : __( 'Upgrade PHP to 8.2+ if possible (performance and compatibility).', 'seotune-search-insights' ) ) . '</td></tr>';

			// max_execution_time.
			$max_exec_label = ( 0 === $max_exec ) ? __( 'unlimited', 'seotune-search-insights' ) : (string) $max_exec . 's';
			$max_exec_action = ( 'ok' === $max_exec_level ) ? __( 'No action needed.', 'seotune-search-insights' ) : ( 'warning' === $max_exec_level ? __( 'Consider increasing max_execution_time to 120s+ for smoother imports.', 'seotune-search-insights' ) : __( 'Increase max_execution_time (or reduce import block size / use server-side runner).', 'seotune-search-insights' ) );
			echo '<tr><td>' . esc_html__( 'max_execution_time (120s+ recommended)', 'seotune-search-insights' ) . '</td><td><span style="' . esc_attr( $this->diagnostic_badge_style_attr( $max_exec_level ) ) . '">' . esc_html( strtoupper( $max_exec_level ) ) . '</span> <code>' . esc_html( $max_exec_label ) . '</code></td><td>' . esc_html( $max_exec_action ) . '</td></tr>';

			// memory_limit.
			$mem_label = $mem_raw ? $mem_raw : ( $mem_b ? (string) $mem_b : 'n/a' );
			$mem_action = ( 'ok' === $mem_level ) ? __( 'No action needed.', 'seotune-search-insights' ) : ( 'warning' === $mem_level ? __( 'Consider raising memory_limit to 256M for large sites.', 'seotune-search-insights' ) : __( 'Increase memory_limit (or lower DB batch size / import block size).', 'seotune-search-insights' ) );
			echo '<tr><td>' . esc_html__( 'memory_limit (256M recommended)', 'seotune-search-insights' ) . '</td><td><span style="' . esc_attr( $this->diagnostic_badge_style_attr( $mem_level ) ) . '">' . esc_html( strtoupper( $mem_level ) ) . '</span> <code>' . esc_html( $mem_label ) . '</code></td><td>' . esc_html( $mem_action ) . '</td></tr>';

			// DISABLE_WP_CRON.
			$cron_level = $disable_wp_cron ? 'warning' : 'ok';
			$cron_action = $disable_wp_cron
				? __( 'Server-side cron is required when DISABLE_WP_CRON is true (call wp-cron.php every 2–5 minutes).', 'seotune-search-insights' )
				: __( 'No action needed. If imports stall on low-traffic sites, add server-side cron (recommended).', 'seotune-search-insights' );
			echo '<tr><td>' . esc_html__( 'DISABLE_WP_CRON', 'seotune-search-insights' ) . '</td><td><span style="' . esc_attr( $this->diagnostic_badge_style_attr( $cron_level ) ) . '">' . esc_html( $disable_wp_cron ? 'WARN' : 'OK' ) . '</span> <code>' . esc_html( $disable_wp_cron ? 'true' : 'false' ) . '</code></td><td>' . esc_html( $cron_action ) . '</td></tr>';

			// Cron late / execution.
			$cron_exec_level = ( $cron_late_backfill || $cron_late_daily || ! $recent_logs_ok ) ? 'warning' : 'ok';
			$cron_exec_action = ( 'ok' === $cron_exec_level )
				? __( 'No action needed.', 'seotune-search-insights' )
				: __( 'Cron may not be executed regularly. Configure server-side cron (recommended) or use WP-CLI runner on the server.', 'seotune-search-insights' );
			$cron_exec_detail = 'backfill_next=' . ( $backfill_next_ts ? gmdate( 'c', (int) $backfill_next_ts ) : 'n/a' ) . ', daily_next=' . ( $daily_next_ts ? gmdate( 'c', (int) $daily_next_ts ) : 'n/a' );
			echo '<tr><td>' . esc_html__( 'Cron execution (WP-Cron must actually run)', 'seotune-search-insights' ) . '</td><td><span style="' . esc_attr( $this->diagnostic_badge_style_attr( $cron_exec_level ) ) . '">' . esc_html( strtoupper( $cron_exec_level ) ) . '</span> <code>' . esc_html( $cron_exec_detail ) . '</code></td><td>' . esc_html( $cron_exec_action ) . '</td></tr>';

			// Traffic / cron triggering.
			$traffic_level = $recent_logs_ok ? 'ok' : 'warning';
			$traffic_action = $recent_logs_ok
				? __( 'No action needed.', 'seotune-search-insights' )
				: __( 'Low traffic can delay WP-Cron. Add server-side cron to call wp-cron.php every 2–5 minutes.', 'seotune-search-insights' );
			echo '<tr><td>' . esc_html__( 'Traffic / WP-Cron triggering', 'seotune-search-insights' ) . '</td><td><span style="' . esc_attr( $this->diagnostic_badge_style_attr( $traffic_level ) ) . '">' . esc_html( strtoupper( $traffic_level ) ) . '</span> <code>' . esc_html( $last_any_log ? $last_any_log : 'no recent logs' ) . '</code></td><td>' . esc_html( $traffic_action ) . '</td></tr>';

			// Caching / security layers (loopback).
			$loop_level = ( true === $loopback_ok ) ? 'ok' : 'warning';
			$loop_action = ( true === $loopback_ok )
				? __( 'No action needed.', 'seotune-search-insights' )
				: __( 'Loopback request may be blocked by WAF/security rules. Allow requests to wp-cron.php and wp-json endpoints.', 'seotune-search-insights' );
			echo '<tr><td>' . esc_html__( 'Caching / security layers (loopback)', 'seotune-search-insights' ) . '</td><td><span style="' . esc_attr( $this->diagnostic_badge_style_attr( $loop_level ) ) . '">' . esc_html( strtoupper( $loop_level ) ) . '</span> <code>' . esc_html( $loopback_note ? $loopback_note : 'n/a' ) . '</code></td><td>' . esc_html( $loop_action ) . '</td></tr>';

			// Time consistency.
			$time_level = $time_ok ? 'ok' : 'warning';
			$time_action = $time_ok ? __( 'No action needed.', 'seotune-search-insights' ) : __( 'Server time appears inconsistent. Fix server clock drift (NTP) to avoid scheduling issues.', 'seotune-search-insights' );
			echo '<tr><td>' . esc_html__( 'Time consistency', 'seotune-search-insights' ) . '</td><td><span style="' . esc_attr( $this->diagnostic_badge_style_attr( $time_level ) ) . '">' . esc_html( strtoupper( $time_level ) ) . '</span> <code>' . esc_html( 'drift=' . (string) $time_drift . 's' ) . '</code></td><td>' . esc_html( $time_action ) . '</td></tr>';

			echo '</tbody></table>';
			echo '</div>';
		}

		// Latest log entries + common issues table.
		$log_table = $wpdb->prefix . 'serpiq_sync_log';
		$logs = array();
		if ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $log_table ) ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$logs = $wpdb->get_results(
				$wpdb->prepare(
					'SELECT created_at, job_type, sync_start_date, sync_end_date, rows_imported, status, duration_ms, error_message FROM %i ORDER BY created_at DESC LIMIT 10',
					$log_table
				),
				ARRAY_A
			);
		}

		// Infer common issues from diagnostics + recent error patterns.
		$issues = array();

		// Prescriptive diagnostics (Milestone 3.2): message + recommended action per scenario.
		$cron = is_array( $health ) && isset( $health['cron'] ) && is_array( $health['cron'] ) ? $health['cron'] : array();
		$cron_scheduled = ! empty( $cron['backfill_step_scheduled'] );
		$cron_next_iso  = isset( $cron['next_run'] ) ? (string) $cron['next_run'] : '';
		$cron_next_ts   = $cron_next_iso ? strtotime( $cron_next_iso ) : false;

		// cron non schedulato.
		if ( in_array( $code, array( 'backfill_no_batches', 'no_recent_logs' ), true ) && ! $cron_scheduled ) {
			$issues['cron_not_scheduled'] = array(
				'title'  => __( 'Cron event is not scheduled.', 'seotune-search-insights' ),
				'text'   => __( 'WordPress has no scheduled event for the historical import, so it cannot continue in background.', 'seotune-search-insights' ),
				'action' => __( 'Enable WP-Cron or configure a server-side cron to call wp-cron.php every few minutes.', 'seotune-search-insights' ),
			);
		}

		// cron schedulato ma in ritardo.
		if ( $cron_scheduled && $cron_next_ts && ( (int) $cron_next_ts < ( time() - 60 ) ) ) {
			$issues['cron_delayed'] = array(
				'title'  => __( 'Cron is scheduled but overdue.', 'seotune-search-insights' ),
				'text'   => __( 'The next backfill step is scheduled in the past, which usually means cron is not executed regularly on this hosting.', 'seotune-search-insights' ),
				'action' => __( 'Configure a real server-side cron (recommended) or use the WP-CLI runner to ensure steps run reliably.', 'seotune-search-insights' ),
			);
		} elseif ( in_array( $code, array( 'backfill_no_recent_batches', 'backfill_no_batches', 'no_recent_logs' ), true ) && $cron_scheduled ) {
			$issues['cron_irregular'] = array(
				'title'  => __( 'Cron may not be executed regularly.', 'seotune-search-insights' ),
				'text'   => __( 'Backfill is marked as running, but recent steps are not completing on a steady cadence.', 'seotune-search-insights' ),
				'action' => __( 'Enable a server-side cron for wp-cron.php, or run the import via WP-CLI on the server.', 'seotune-search-insights' ),
			);
		}

		// backfill stuck.
		if ( 'backfill_stuck' === $code ) {
			$issues['backfill_stuck'] = array(
				'title'  => __( 'Historical import appears stuck.', 'seotune-search-insights' ),
				'text'   => __( 'Progress did not advance for a while.', 'seotune-search-insights' ),
				'action' => __( 'Use “Resume from current data” (no data loss). If it stays stuck, use “Force next step”, then verify WP-Cron/server cron.', 'seotune-search-insights' ),
			);
		}

		// Hosting / environment hints from last health check.
		if ( is_array( $health ) && ! empty( $health['environment_flag'] ) && 'restricted_hosting' === $health['environment_flag'] ) {
			$issues['restricted_hosting'] = array(
				'title'  => __( 'Hosting environment appears resource-constrained.', 'seotune-search-insights' ),
				'text'   => __( 'Health checks report PHP or database resource limits (timeouts, memory, or connection issues).', 'seotune-search-insights' ),
				'action' => __( 'Apply the Shared hosting preset (or set Days per block to 3–5, Rows per DB batch to 500, Delay to 60s). If errors persist, ask your hosting provider to increase PHP limits (memory / max_execution_time) and DB resources.', 'seotune-search-insights' ),
			);
		}

		if ( ! empty( $logs ) ) {
			$gsc_quota = false;
			$gsc_invalid_grant = false;
			$php_db_error = false;

			foreach ( $logs as $row ) {
				if ( empty( $row['status'] ) || 'error' !== $row['status'] ) {
					continue;
				}
				$msg = isset( $row['error_message'] ) ? strtolower( (string) $row['error_message'] ) : '';
				if ( '' === $msg ) {
					continue;
				}

				if ( strpos( $msg, 'invalid_grant' ) !== false ) {
					$gsc_invalid_grant = true;
				}
				if ( strpos( $msg, 'quota' ) !== false || strpos( $msg, 'user ratelimit' ) !== false || strpos( $msg, 'rate limit' ) !== false ) {
					$gsc_quota = true;
				}
				if (
					strpos( $msg, 'max_execution_time' ) !== false ||
					strpos( $msg, 'maximum execution time' ) !== false ||
					strpos( $msg, 'allowed memory size' ) !== false ||
					strpos( $msg, 'out of memory' ) !== false ||
					strpos( $msg, 'mysql server has gone away' ) !== false ||
					strpos( $msg, 'deadlock found' ) !== false
				) {
					$php_db_error = true;
				}
			}

			// invalid_grant → ricollega Google.
			if ( $gsc_invalid_grant || ( is_array( $health ) && ( ( $health['gsc_error'] ?? '' ) === 'invalid_grant' ) ) ) {
				$issues['gsc_invalid_grant'] = array(
					'title'  => __( 'Google connection expired (invalid_grant).', 'seotune-search-insights' ),
					'text'   => __( 'Google OAuth tokens are no longer valid, so Search Console requests cannot be completed.', 'seotune-search-insights' ),
					'action' => __( 'Disconnect and reconnect Google in Settings → Google Search Console, then resume or restart the import.', 'seotune-search-insights' ),
				);
			}

			// quota/rate-limit → riduci blocchi / attendi reset.
			if ( $gsc_quota || ( is_array( $health ) && ( ( $health['gsc_error'] ?? '' ) === 'quota_or_ratelimit' ) ) ) {
				$issues['gsc_quota'] = array(
					'title'  => __( 'Search Console API quota or rate limits.', 'seotune-search-insights' ),
					'text'   => __( 'Some recent imports failed due to Google Search Console quota / rate-limit constraints.', 'seotune-search-insights' ),
					'action' => __( 'Reduce Days per block and run imports at a quieter time, or wait for quotas to reset. If needed, use a dedicated Google Cloud project.', 'seotune-search-insights' ),
				);
			}

			if ( $php_db_error ) {
				$issues['php_db'] = array(
					'title'  => __( 'Server resource or database limits.', 'seotune-search-insights' ),
					'text'   => __( 'Recent errors mention timeouts, memory, or database connection issues.', 'seotune-search-insights' ),
					'action' => __( 'Apply the Shared hosting preset (smaller blocks, fewer DB rows per batch, longer delay). If it still fails, ask your hosting provider to increase PHP limits (memory / max_execution_time) and DB resources.', 'seotune-search-insights' ),
				);
			}
		}

		if ( ! empty( $issues ) ) {
			echo '<div style="margin:8px 0 4px 0;">';
			echo '<p style="margin:0 0 4px 0; font-size:12px; font-weight:600;">' . esc_html__( 'Common issues (detected)', 'seotune-search-insights' ) . '</p>';
			echo '<ul style="margin:0 0 0 16px; padding:0; list-style:disc; font-size:12px; color:var(--serpiq-text-muted);">';
			foreach ( $issues as $item ) {
				echo '<li style="margin:2px 0;">';
				if ( ! empty( $item['title'] ) ) {
					echo '<strong>' . esc_html( $item['title'] ) . '</strong> ';
				}
				if ( ! empty( $item['text'] ) ) {
					echo esc_html( $item['text'] );
				}
				if ( ! empty( $item['action'] ) ) {
					echo ' <strong>' . esc_html__( 'Recommended action:', 'seotune-search-insights' ) . '</strong> ' . esc_html( $item['action'] );
				}
				echo '</li>';
			}
			echo '</ul>';
			echo '</div>';
		}

		echo '<p style="margin:8px 0 4px 0; font-size:12px; font-weight:600;">' . esc_html__( 'Recent activity (log)', 'seotune-search-insights' ) . '</p>';
		if ( ! empty( $logs ) ) {
			echo '<table class="widefat striped" style="margin-top:8px; font-size:12px;">';
			echo '<thead><tr>';
			echo '<th>' . esc_html__( 'Time (UTC)', 'seotune-search-insights' ) . '</th>';
			echo '<th>' . esc_html__( 'Type', 'seotune-search-insights' ) . '</th>';
			echo '<th>' . esc_html__( 'Range', 'seotune-search-insights' ) . '</th>';
			echo '<th style="text-align:right;">' . esc_html__( 'Rows', 'seotune-search-insights' ) . '</th>';
			echo '<th style="text-align:right;">' . esc_html__( 'Duration (ms)', 'seotune-search-insights' ) . '</th>';
			echo '<th>' . esc_html__( 'Result', 'seotune-search-insights' ) . '</th>';
			echo '</tr></thead><tbody>';

			$raw_lines = array();
			foreach ( $logs as $row ) {
				$created = (string) ( $row['created_at'] ?? '' );
				$job_type = (string) ( $row['job_type'] ?? '' );
				$range = '';
				if ( ! empty( $row['sync_start_date'] ) || ! empty( $row['sync_end_date'] ) ) {
					$range = trim( (string) ( $row['sync_start_date'] ?? '' ) ) . ' → ' . trim( (string) ( $row['sync_end_date'] ?? '' ) );
				}
				$rows_imported = (int) ( $row['rows_imported'] ?? 0 );
				$duration_ms   = (int) ( $row['duration_ms'] ?? 0 );
				$status_label  = (string) ( $row['status'] ?? '' );

				// Human label for job_type.
				switch ( $job_type ) {
					case 'backfill_batch':
						$type_label = __( 'Historical import batch', 'seotune-search-insights' );
						break;
					case 'internal_links_daily':
						$type_label = __( 'Internal links daily index', 'seotune-search-insights' );
						break;
					case 'sync':
					default:
						$type_label = __( 'Sync', 'seotune-search-insights' );
						break;
				}

				echo '<tr>';
				echo '<td>' . esc_html( $created ) . '</td>';
				echo '<td>' . esc_html( $type_label ) . '</td>';
				echo '<td>' . esc_html( $range ) . '</td>';
				echo '<td style="text-align:right;">' . esc_html( number_format_i18n( $rows_imported ) ) . '</td>';
				echo '<td style="text-align:right;">' . esc_html( $duration_ms ) . '</td>';
				echo '<td>';
				if ( 'error' === $status_label ) {
					echo '<span style="color:var(--serpiq-error); font-weight:500;">' . esc_html( ucfirst( $status_label ) ) . '</span>';
				} else {
					echo esc_html( ucfirst( $status_label ) );
				}
				echo '</td>';
				echo '</tr>';

				// Build plain-text log line for support copy.
				$line = sprintf(
					'%1$s | %2$s | %3$s | rows=%4$d | status=%5$s | duration_ms=%6$d',
					$created,
					$job_type,
					$range,
					$rows_imported,
					$status_label,
					$duration_ms
				);
				if ( ! empty( $row['error_message'] ) ) {
					// Collapse whitespace and truncate a bit.
					$msg = wp_trim_words( preg_replace( '/\s+/', ' ', (string) $row['error_message'] ), 20, '…' );
					$line .= ' | msg=' . $msg;
				}
				$raw_lines[] = $line;
			}
			echo '</tbody></table>';

			$raw_text    = implode( "\n", $raw_lines );

			// Copy (support): Import diagnostics + Recent activity (log) only.
			$copy_lines = array();
			$copy_lines[] = '=== Seotune Search Insights Diagnostics ===';
			$copy_lines[] = 'Generated at (UTC): ' . gmdate( 'c' );
			$copy_lines[] = '';
			$copy_lines[] = '--- Import diagnostics ---';
			$copy_lines[] = 'WordPress: ' . get_bloginfo( 'version' );
			$copy_lines[] = 'PHP: ' . PHP_VERSION;
			$copy_lines[] = 'max_execution_time: ' . (string) ini_get( 'max_execution_time' );
			$copy_lines[] = 'memory_limit: ' . (string) ini_get( 'memory_limit' );
			$copy_lines[] = 'DISABLE_WP_CRON: ' . ( ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) ? 'true' : 'false' );
			$next_backfill_ts = wp_next_scheduled( 'serpiq_backfill_step' );
			$next_daily_ts    = wp_next_scheduled( 'serpiq_daily_sync' );
			$copy_lines[] = 'serpiq_backfill_step next_run (UTC): ' . ( $next_backfill_ts ? gmdate( 'c', (int) $next_backfill_ts ) : 'n/a' );
			$copy_lines[] = 'serpiq_daily_sync next_run (UTC): ' . ( $next_daily_ts ? gmdate( 'c', (int) $next_daily_ts ) : 'n/a' );
			if ( ! empty( $issues ) ) {
				$copy_lines[] = '';
				$copy_lines[] = 'Common issues (detected):';
				foreach ( $issues as $it ) {
					$t = isset( $it['title'] ) ? (string) $it['title'] : '';
					$a = isset( $it['action'] ) ? (string) $it['action'] : '';
					$copy_lines[] = '- ' . trim( $t . ( $a ? ' | action: ' . $a : '' ) );
				}
			}
			$copy_lines[] = '';
			$copy_lines[] = '--- Recent activity (log) ---';
			$copy_lines[] = $raw_text ? $raw_text : '(none)';
			$copy_text = trim( implode( "\n", $copy_lines ) );

			echo '<div style="margin-top:8px; display:flex; justify-content:flex-end; gap:8px; flex-wrap:wrap;">';
			echo '<button type="button" class="serpiq-btn serpiq-btn-secondary" onclick="(function(){var t=document.getElementById(\'serpiq-diagnostics-copy\');if(!t){return;}var v=(t.textContent||t.innerText||\'\').trim();if(!v){alert(\'Nothing to copy.\');return;}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(v).then(function(){alert(\'Copied to clipboard.\');}).catch(function(){alert(\'Copy failed.\');});}else{var r=document.createRange();r.selectNodeContents(t);var s=window.getSelection();s.removeAllRanges();s.addRange(r);try{document.execCommand(\'copy\');alert(\'Copied to clipboard.\');}catch(e){alert(\'Copy failed.\');}}})();">';
			echo esc_html__( 'Copy diagnostics + log', 'seotune-search-insights' );
			echo '</button>';
			echo '</div>';

			echo '<pre id="serpiq-diagnostics-copy" style="position:absolute; left:-9999px; top:auto; width:1px; height:1px; overflow:hidden;">' . esc_html( $copy_text ) . '</pre>';
		} else {
			echo '<p style="margin:4px 0 0 0; font-size:13px; color:var(--serpiq-text-muted);">' . esc_html__( 'No sync or import activity has been recorded yet.', 'seotune-search-insights' ) . '</p>';
		}

		echo '</div>'; // End diagnostics card.
		} // End if $show_diagnostics.

		// DANGER ZONE: Reset all imported data.
		echo '<div style="margin-top:16px; padding-top:16px; border-top:1px solid var(--serpiq-border);">';
		echo '<h3 style="margin:0 0 8px 0; font-size:14px; font-weight:600; color: var(--serpiq-error);">' . esc_html__( 'Danger zone', 'seotune-search-insights' ) . '</h3>';
		echo '<p style="margin:0 0 12px 0; color:var(--serpiq-text-muted); font-size: 13px;">' . esc_html__( 'This permanently deletes all imported Google Search Console data stored by Seotune Search Insights (queries, pages, totals, alerts, sync log, internal links). OAuth connection and API keys are kept.', 'seotune-search-insights' ) . '</p>';
		echo '<form method="post" action="" style="margin:0;">';
		wp_nonce_field( 'seotune_reset_all_gsc_data', 'seotune_nonce' );
		echo '<input type="hidden" name="serpiq_reset_all_gsc_data" value="1"/>';
		echo '<button type="submit" class="serpiq-btn serpiq-btn-secondary" style="border-color: var(--serpiq-error); color: var(--serpiq-error);" ';
		echo 'onclick="if(!confirm(\'This will DELETE all imported Search Console data. Continue?\')){return false;} if(!confirm(\'Are you absolutely sure? This cannot be undone.\')){return false;} return true;">';
		echo esc_html__( 'Reset (delete all imported data)', 'seotune-search-insights' );
		echo '</button>';
		echo '</form>';
		echo '</div>';

		echo '</div>'; // End Sync & Import card.
		echo '</div>'; // End panel sync-import.

		// Save settings in fondo alla pagina (usa il form principale in alto).
		echo '<div style="display:flex; justify-content:flex-end; margin-top:24px;">';
		echo '<button type="submit" form="serpiq-settings-form" name="serpiq_settings_save" value="1" class="serpiq-btn serpiq-btn-primary">' . esc_html__( 'Save settings', 'seotune-search-insights' ) . '</button>';
		echo '</div>';

		echo '</div></div>'; // .serpiq-page + #serpiq-admin-app
		echo '</div>'; // .wrap
	}

	public function enqueue_assets( string $hook_suffix ): void {
		if ( ! current_user_can( SERPIQ_CAPABILITY ) ) {
			return;
		}

		if ( strpos( $hook_suffix, 'serpiq' ) === false ) {
			return;
		}

		$asset_url = SERPIQ_PLUGIN_URL . 'admin/assets/';
		$ver = SERPIQ_VERSION;

		// Built React app (admin-app.js + admin-app.css from react-app build).
		$built_css    = $asset_url . 'js/admin-app.css';
		$built_js     = $asset_url . 'js/admin-app.js';
		$built_js_path  = SERPIQ_PLUGIN_DIR . 'admin/assets/js/admin-app.js';
		$built_css_path = SERPIQ_PLUGIN_DIR . 'admin/assets/js/admin-app.css';
		$use_built = file_exists( $built_js_path );
		$is_settings = ( strpos( $hook_suffix, 'serpiq-settings' ) !== false );

		// When the bundle is rebuilt, SERPIQ_VERSION may stay the same; use filemtime so
		// browsers/CDNs fetch the new admin-app.js instead of a cached copy.
		if ( $use_built ) {
			$m_js  = @filemtime( $built_js_path );
			$m_css = @filemtime( $built_css_path );
			$built_mtime = max( (int) $m_js, (int) $m_css );
			if ( $built_mtime > 0 ) {
				$ver = (string) $built_mtime;
			}
		}

		if ( $use_built ) {
			// Admin UI uses a local system font stack (no Google Fonts / third-party CSS).
			wp_enqueue_style( 'serpiq-admin', $built_css, array(), $ver );
			if ( $is_settings ) {
				wp_enqueue_style( 'serpiq-settings', $asset_url . 'css/serpiq-settings.css', array( 'serpiq-admin' ), $ver );
				wp_enqueue_script( 'serpiq-settings-tabs', $asset_url . 'js-runtime/serpiq-settings-tabs.js', array(), $ver, true );
			}
			// Do not mount React app on the Settings screen (it is rendered by PHP).
			if ( ! $is_settings ) {
				wp_enqueue_script(
					'serpiq-admin',
					$built_js,
					array( 'wp-i18n' ),
					$ver,
					true
				);
			}
		} else {
			wp_enqueue_style( 'serpiq-admin', $asset_url . 'css/admin.css', array(), $ver );
			if ( $is_settings ) {
				wp_enqueue_style( 'serpiq-settings', $asset_url . 'css/serpiq-settings.css', array( 'serpiq-admin' ), $ver );
				wp_enqueue_script( 'serpiq-settings-tabs', $asset_url . 'js-runtime/serpiq-settings-tabs.js', array(), $ver, true );
			}
			if ( ! $is_settings ) {
				wp_enqueue_script(
					'serpiq-admin',
					$asset_url . 'js/admin-placeholder.js',
					array( 'wp-i18n' ),
					$ver,
					true
				);
			}
		}

		if ( ! $is_settings ) {
			wp_set_script_translations( 'serpiq-admin', 'seotune-search-insights', SERPIQ_PLUGIN_DIR . 'languages' );
			// React admin screens: wp_rest nonce for every serpiq/v1 fetch (see admin/react-app/src/api.js).
			// Settings is PHP-only; OAuth connect/disconnect use admin GET links + wp_nonce_url().
			// Optional REST /oauth/connect-url also uses wp_rest; Google callback uses OAuth state, not these nonces.
			wp_localize_script(
				'serpiq-admin',
				'SERPIQ_ADMIN',
				array(
					'restUrl'         => esc_url_raw( rest_url( 'serpiq/v1' ) ),
					'nonce'           => wp_create_nonce( 'wp_rest' ),
					'currentPage'     => $this->get_serpiq_current_page( $hook_suffix ),
					'adminUrl'        => esc_url_raw( admin_url( 'admin.php' ) ),
					'property'        => (string) get_option( Options::google_property_key(), '' ),
					'brandLogoUrl'    => esc_url_raw( SERPIQ_PLUGIN_URL . 'admin/assets/brand/header-logo.png' ),
				)
			);
		}
	}

	/**
	 * Trusted inline CSS for diagnostic badges (runtime table). Values are literals only.
	 */
	private function diagnostic_badge_style_attr( string $level ): string {
		switch ( $level ) {
			case 'ok':
				return 'display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;text-transform:uppercase;letter-spacing:0.05em;background:rgba(5,150,105,0.12);color:#065f46;';
			case 'warning':
				return 'display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;text-transform:uppercase;letter-spacing:0.05em;background:rgba(234,179,8,0.14);color:#92400e;';
			case 'error':
				return 'display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;text-transform:uppercase;letter-spacing:0.05em;background:rgba(220,38,38,0.12);color:#991b1b;';
			default:
				return 'display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;text-transform:uppercase;letter-spacing:0.05em;background:rgba(15,23,42,0.06);color:var(--serpiq-text-muted);';
		}
	}

	private function get_serpiq_current_page( string $hook_suffix ): string {
		if ( strpos( $hook_suffix, 'serpiq-keywords' ) !== false ) {
			return 'keywords';
		}
		if ( strpos( $hook_suffix, 'serpiq-pages' ) !== false ) {
			return 'pages';
		}
		if ( strpos( $hook_suffix, 'serpiq-opportunities' ) !== false ) {
			return 'opportunities';
		}
		if ( strpos( $hook_suffix, 'serpiq-cannibalization' ) !== false ) {
			return 'cannibalization';
		}
		if ( strpos( $hook_suffix, 'serpiq-decay' ) !== false ) {
			return 'decay';
		}
		if ( strpos( $hook_suffix, 'serpiq-content-refresh' ) !== false ) {
			return 'content-refresh';
		}
		if ( strpos( $hook_suffix, 'serpiq-internal-links' ) !== false ) {
			return 'internal-links';
		}
		if ( strpos( $hook_suffix, 'serpiq-question-queries' ) !== false ) {
			return 'question-queries';
		}
		return 'dashboard';
	}
}

