<?php
/**
 * Admin panel: settings pages, AJAX handlers, and cookie registry management.
 *
 * @package LetsCodeCookieBanner
 */

defined( 'ABSPATH' ) || exit;

/**
 * Manages the plugin admin interface, settings, and all admin AJAX endpoints.
 */
class LCCB_Admin {

	/**
	 * WordPress hook suffix for the plugin submenu page.
	 *
	 * @var string
	 */
	private $submenu_hook = '';

	/**
	 * Registers WordPress admin hooks.
	 */
	public function __construct() {
		add_action( 'admin_menu', array( $this, 'add_menu' ) );
		add_action( 'admin_init', array( $this, 'maybe_redirect_parent' ) );
		add_action( 'admin_init', array( $this, 'register_settings' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
		add_action( 'wp_ajax_lccb_export_logs', array( $this, 'export_logs' ) );
		add_action( 'wp_ajax_lccb_delete_old_logs', array( $this, 'ajax_delete_old_logs' ) );
		add_action( 'wp_ajax_lccb_cookie_save', array( $this, 'ajax_cookie_save' ) );
		add_action( 'wp_ajax_lccb_cookie_delete', array( $this, 'ajax_cookie_delete' ) );
		add_action( 'wp_ajax_lccb_cookie_reset', array( $this, 'ajax_cookie_reset' ) );
		add_action( 'wp_ajax_lccb_cookie_add_detected', array( $this, 'ajax_cookie_add_detected' ) );
		add_action( 'wp_ajax_lccb_clear_detected', array( $this, 'ajax_clear_detected' ) );
		add_action( 'wp_ajax_lccb_server_scan', array( $this, 'ajax_server_scan' ) );
		add_action( 'wp_ajax_lccb_scan_third_party', array( $this, 'ajax_scan_third_party' ) );
		add_action( 'wp_ajax_lccb_clear_third_party_domains', array( $this, 'ajax_clear_third_party_domains' ) );
		add_action( 'wp_ajax_lccb_api_reconnect', array( $this, 'ajax_api_reconnect' ) );
		add_action( 'wp_ajax_lccb_api_lookup', array( $this, 'ajax_api_lookup' ) );
		add_action( 'wp_ajax_lccb_api_contribute', array( $this, 'ajax_api_contribute' ) );
		add_action( 'wp_ajax_lccb_api_contribute_all', array( $this, 'ajax_api_contribute_all' ) );
		add_action( 'wp_ajax_lccb_suggest_scripts', array( $this, 'ajax_suggest_scripts' ) );
		add_action( 'wp_ajax_lccb_clear_detected_scripts', array( $this, 'ajax_clear_detected_scripts' ) );
		add_action( 'wp_ajax_lccb_record_page_scan', array( $this, 'ajax_record_page_scan' ) );
		add_action( 'wp_ajax_lccb_cookie_add_all_detected', array( $this, 'ajax_cookie_add_all_detected' ) );
		add_filter( 'plugin_action_links_' . plugin_basename( LCCB_PLUGIN_FILE ), array( $this, 'add_settings_link' ) );
	}

	/**
	 * Prepends a Settings link to the plugin action links on the Plugins page.
	 *
	 * @param array $links Existing action links.
	 * @return array
	 */
	public function add_settings_link( $links ) {
		$url = admin_url( 'admin.php?page=lccb-settings' );
		array_unshift( $links, '<a href="' . esc_url( $url ) . '">' . esc_html__( 'Settings', 'lets-code-cookie-banner' ) . '</a>' );
		return $links;
	}

	/**
	 * Registers the top-level "Let's Code Tools" menu and the Cookie Banner submenu.
	 */
	public function add_menu() {
		// Top-level "Let's Code Tools" menu shared by all Let's Code plugins.
		add_menu_page(
			esc_html__( "Let's Code Tools", 'lets-code-cookie-banner' ),
			esc_html__( "Let's Code Tools", 'lets-code-cookie-banner' ),
			'manage_options',
			'lets-code-tools',
			array( $this, 'render_page' ),
			'dashicons-admin-tools',
			80
		);

		// Remove the auto-created first submenu that duplicates the parent label.
		remove_submenu_page( 'lets-code-tools', 'lets-code-tools' );

		$this->submenu_hook = add_submenu_page(
			'lets-code-tools',
			esc_html__( "Let's Code Cookie Banner", 'lets-code-cookie-banner' ),
			esc_html__( 'Cookie Banner', 'lets-code-cookie-banner' ),
			'manage_options',
			'lccb-settings',
			array( $this, 'render_page' )
		);
	}

	/**
	 * Redirects the parent "Let's Code Tools" menu page to the Cookie Banner submenu.
	 */
	public function maybe_redirect_parent() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( is_admin() && isset( $_GET['page'] ) && 'lets-code-tools' === $_GET['page'] ) {
			wp_safe_redirect( admin_url( 'admin.php?page=lccb-settings' ) );
			exit;
		}
	}

	/**
	 * Registers the lccb_settings option with WordPress Settings API.
	 */
	public function register_settings() {
		register_setting(
			'lccb_settings_group',
			'lccb_settings',
			array( 'sanitize_callback' => array( $this, 'sanitize_settings' ) )
		);
	}

	/**
	 * Sanitises and validates the settings form input before saving.
	 *
	 * @param array $input Raw form values submitted by the user.
	 * @return array Sanitised settings array.
	 */
	public function sanitize_settings( $input ) {
		if ( ! current_user_can( 'manage_options' ) ) {
			add_settings_error( 'lccb_settings', 'unauthorized', esc_html__( 'Insufficient permissions.', 'lets-code-cookie-banner' ) );
			return get_option( 'lccb_settings', array() );
		}

		// Start from existing settings so each tab only touches its own fields.
		$out = get_option( 'lccb_settings', array() );
		$tab = isset( $input['_tab'] ) ? sanitize_key( $input['_tab'] ) : '';

		if ( 'general' === $tab ) {
			$out['banner_enabled']      = ! empty( $input['banner_enabled'] ) ? '1' : '0';
			$out['show_fab_button']     = ! empty( $input['show_fab_button'] ) ? '1' : '0';
			$out['logging_enabled']     = ! empty( $input['logging_enabled'] ) ? '1' : '0';
			$out['delete_on_uninstall'] = ! empty( $input['delete_on_uninstall'] ) ? '1' : '0';
			$out['show_powered_by']     = ! empty( $input['show_powered_by'] ) ? '1' : '0';
			$out['cookie_policy_url']   = isset( $input['cookie_policy_url'] ) ? esc_url_raw( $input['cookie_policy_url'] ) : '';
		}

		return apply_filters( 'lccb_sanitize_extra_settings', $out, $input, $tab );
	}

	/**
	 * Enqueues admin CSS and JS assets on the plugin settings page.
	 *
	 * @param string $hook Current admin page hook suffix.
	 */
	public function enqueue_assets( $hook ) {
		if ( $hook !== $this->submenu_hook ) {
			return;
		}
		wp_enqueue_style( 'wp-color-picker' );
		wp_enqueue_style( 'lccb-admin', LCCB_PLUGIN_URL . 'assets/css/lccb-admin.css', array(), LCCB_VERSION );
		wp_enqueue_script( 'lccb-admin', LCCB_PLUGIN_URL . 'assets/js/lccb-admin.js', array( 'jquery', 'wp-color-picker' ), LCCB_VERSION, true );

		wp_localize_script(
			'lccb-admin',
			'lccbAdmin',
			array(
				'ajax_url'                 => admin_url( 'admin-ajax.php' ),
				'delete_nonce'             => wp_create_nonce( 'lccb_delete_logs_nonce' ),
				'cookie_nonce'             => wp_create_nonce( 'lccb_cookie_registry_nonce' ),
				'server_scan_nonce'        => wp_create_nonce( 'lccb_server_scan_nonce' ),
				'api_nonce'                => wp_create_nonce( 'lccb_api_nonce' ),
				'confirmAll'               => esc_html__( 'Are you sure you want to delete ALL recorded consents? This action is irreversible.', 'lets-code-cookie-banner' ),
				'confirmOld'               => esc_html__( 'Are you sure you want to delete all consents older than {period}? This action is irreversible.', 'lets-code-cookie-banner' ),
				'errorMsg'                 => esc_html__( 'An error occurred during deletion.', 'lets-code-cookie-banner' ),
				'confirmReset'             => esc_html__( 'Are you sure you want to reset all Design values to their defaults? Unsaved changes will be lost.', 'lets-code-cookie-banner' ),
				'confirmCookieDelete'      => esc_html__( 'Delete this cookie from the list?', 'lets-code-cookie-banner' ),
				'confirmCookieReset'       => esc_html__( 'Clear all cookies from the registry? This cannot be undone.', 'lets-code-cookie-banner' ),
				'confirmClearDetected'     => esc_html__( 'Clear the list of detected cookies? They will be re-detected on the next site visit.', 'lets-code-cookie-banner' ),
				'confirmBulkContribute'    => esc_html__( 'Send all unknown cookies for review by the Let\'s Code team? Cookies already in the global database will be skipped.', 'lets-code-cookie-banner' ),
				'msgBulkContributeSuccess' => esc_html__( 'All unknown cookies sent for review. Thank you for contributing!', 'lets-code-cookie-banner' ),
				'msgBulkContributeNone'    => esc_html__( 'No unknown cookies to send — all registry cookies already come from the global database.', 'lets-code-cookie-banner' ),
				'apiOfflineMsg'            => esc_html__( 'The service is temporarily unavailable. Please try again later or contact support@let-scode.it', 'lets-code-cookie-banner' ),
				'msgEnterUrl'              => esc_html__( 'Please enter a URL before starting the scan.', 'lets-code-cookie-banner' ),
				'msgInvalidUrl'            => esc_html__( 'Invalid URL. Please enter a full URL (e.g. https://example.com/page/).', 'lets-code-cookie-banner' ),
				'msgPopupBlocked'          => esc_html__( 'The popup was blocked by the browser.', 'lets-code-cookie-banner' ),
				'msgPopupBlockedLink'      => esc_html__( 'Click here to open the scan manually', 'lets-code-cookie-banner' ),
				'msgScanStarted'           => esc_html__( 'Scan window opened. Please wait for it to complete before closing it — cookies not yet in your registry will appear under "Cookies Found by Scans".', 'lets-code-cookie-banner' ),
				'msgNetworkErrorScan'      => esc_html__( 'Network error during scan.', 'lets-code-cookie-banner' ),
				'msgNetworkError'          => esc_html__( 'Network error.', 'lets-code-cookie-banner' ),
				'msgErrorPrefix'           => esc_html__( 'Error:', 'lets-code-cookie-banner' ),
				// translators: %d = number of cookies detected.
				'msgServerScanFound'       => esc_html__( 'Detected %d cookies (including HttpOnly). Refresh the page to see cookies not yet in your registry under "Cookies Found by Scans".', 'lets-code-cookie-banner' ),
				'msgServerScanNone'        => esc_html__( 'No Set-Cookie headers found in the HTTP response. The page may not set cookies on first load.', 'lets-code-cookie-banner' ),
				// translators: %1$s = number of domains scanned, %2$s = number of cookies detected (replaced in JavaScript).
				'msgThirdPartyScanDone'    => esc_html__( 'Scan complete: %1$s domains scanned, %2$s cookies detected.', 'lets-code-cookie-banner' ),
				'msgThirdPartyScanFail'    => esc_html__( 'Unreachable:', 'lets-code-cookie-banner' ),
				'msgThirdPartyScanMore'    => esc_html__( 'Refresh the page to see cookies not yet in your registry under "Cookies Found by Scans".', 'lets-code-cookie-banner' ),
				'msgCookieNameRequired'    => esc_html__( 'Cookie name is required.', 'lets-code-cookie-banner' ),
				'msgNameInRegistry'        => esc_html__( 'A cookie with this name is already in the registry.', 'lets-code-cookie-banner' ),
				'msgCookieUpdated'         => esc_html__( 'Cookie updated.', 'lets-code-cookie-banner' ),
				'msgCookieAdded'           => esc_html__( 'Cookie added to the registry.', 'lets-code-cookie-banner' ),
				'msgInRegistry'            => esc_html__( 'In registry', 'lets-code-cookie-banner' ),
				// translators: %s = date/time of the last database update.
				'msgLookupLastUpdate'      => esc_html__( 'Last check: %s', 'lets-code-cookie-banner' ),
				'msgLookupUpdating'        => esc_html__( 'Updating…', 'lets-code-cookie-banner' ),
				// translators: %d = number of cookies updated.
				'msgLookupUpdated'         => esc_html__( '%d cookies checked and updated.', 'lets-code-cookie-banner' ),
				'msgLookupNoResults'       => esc_html__( 'No matches found in the global database. Use "Send unknown cookies for check" to submit them for review.', 'lets-code-cookie-banner' ),
				// translators: %1$d = number of cookies updated, %2$d = number still unidentified.
				'msgLookupPartial'         => esc_html__( '%1$d cookies checked and updated. %2$d cookie(s) not found in the global database — use "Send unknown cookies for check" to submit them for review.', 'lets-code-cookie-banner' ),
				// translators: %d = number of log records deleted.
				'msgLogsDeleted'           => esc_html__( '%d records deleted.', 'lets-code-cookie-banner' ),
				'modalTitleEdit'           => esc_html__( 'Edit Cookie', 'lets-code-cookie-banner' ),
				'modalTitleAdd'            => esc_html__( 'Add Cookie', 'lets-code-cookie-banner' ),
				'modalTitleDetected'       => esc_html__( 'Edit Detected Cookie', 'lets-code-cookie-banner' ),
				'modalSaveEdit'            => esc_html__( 'Update Cookie', 'lets-code-cookie-banner' ),
				'modalSaveNew'             => esc_html__( 'Add Cookie', 'lets-code-cookie-banner' ),
				'modalSaveDetected'        => esc_html__( 'Add to Registry', 'lets-code-cookie-banner' ),
				// translators: %d = number of scripts sent.
				'msgSuggestScriptsSent'    => esc_html__( '%d script(s) sent successfully. Thank you for helping improve the plugin!', 'lets-code-cookie-banner' ),
				'msgSuggestScriptsAllDone' => esc_html__( 'All detected scripts have already been synced.', 'lets-code-cookie-banner' ),
				'confirmClearScripts'      => esc_html__( 'Clear the list of detected scripts? This cannot be undone.', 'lets-code-cookie-banner' ),
				// translators: %s = date/time of last sync.
				'suggestScriptsLastSync'   => esc_html__( 'Last sync: %s', 'lets-code-cookie-banner' ),
				'scan_page_nonce'          => wp_create_nonce( 'lccb_record_page_scan_nonce' ),
				'confirmAddAll'            => esc_html__( 'Add all detected cookies to the registry? Cookies already in the registry will be skipped.', 'lets-code-cookie-banner' ),
				// translators: %d = number of cookies added.
				'msgAddAllDone'            => esc_html__( '%d cookie(s) added to the registry.', 'lets-code-cookie-banner' ),
				'msgAddAllNone'            => esc_html__( 'No new cookies to add — all detected cookies are already in the registry.', 'lets-code-cookie-banner' ),
				'registryNames'            => array_values( array_filter( wp_list_pluck( LCCB_Cookie_Registry::get_all(), 'name' ), 'strlen' ) ),
				'designDefaults'           => array(
					'bg_color'            => '#ffffff',
					'text_color'          => '#333333',
					'btn_bg'              => '#1a73e8',
					'btn_text'            => '#ffffff',
					'btn_border_color'    => '#1a73e8',
					'overlay_bg'          => '#f8f8f8',
					'always_active_color' => '#2e7d32',
					'fab_bg'              => '#1a73e8',
					'fab_icon_color'      => '#ffffff',
					'fab_border_color'    => '#1a73e8',
					'btn_border_radius'   => 8,
					'btn_padding_v'       => 10,
					'btn_padding_h'       => 18,
					'fab_icon_size'       => 22,
					'banner_left'         => 20,
					'banner_right'        => 20,
					'banner_bottom'       => 20,
					'banner_radius'       => 8,
					'font_size'           => 14,
					'table_header_bg'     => '#f5f5f5',
					'table_header_text'   => '#333333',
					'table_odd_bg'        => '#ffffff',
					'table_odd_text'      => '#000000',
					'table_even_bg'       => '#ffffff',
					'table_even_text'     => '#000000',
					'fab_position'        => 'bottom-left',
					'fab_bottom'          => 20,
					'fab_left'            => 20,
					'fab_right'           => 20,
					'fab_border_width'    => 0,
					'btn_border_width'    => 2,
					'fab_box_shadow'      => '0 2px 10px rgba(0,0,0,.25)',
				),
			)
		);
		do_action( 'lccb_admin_enqueue_assets', $this->submenu_hook );
	}

	/**
	 * AJAX handler: streams the consent log as a CSV file download.
	 */
	public function export_logs() {
		check_ajax_referer( 'lccb_export_logs_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Insufficient permissions.', 'lets-code-cookie-banner' ) );
		}

		$logs = LCCB_Consent_Manager::get_logs( 50000, 1 );

		header( 'Content-Type: text/csv; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename=cookie-consents-' . gmdate( 'Y-m-d' ) . '.csv' );
		header( 'Pragma: no-cache' );
		header( 'Expires: 0' );

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
		$output = fopen( 'php://output', 'w' );
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fputcsv
		fputcsv( $output, array( 'ID', 'Consent ID', 'Date', 'Necessary', 'Statistics', 'Marketing', 'Preferences', 'Action' ) );
		foreach ( $logs as $log ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fputcsv
			fputcsv(
				$output,
				array(
					(int) $log->id,
					$log->consent_id,
					$log->consent_date,
					$log->necessary ? 'Yes' : 'No',
					$log->statistics ? 'Yes' : 'No',
					$log->marketing ? 'Yes' : 'No',
					$log->preferences ? 'Yes' : 'No',
					$log->action,
				)
			);
		}
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
		fclose( $output );
		exit;
	}

	/**
	 * AJAX handler: deletes consent logs older than the requested period.
	 */
	public function ajax_delete_old_logs() {
		check_ajax_referer( 'lccb_delete_logs_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Insufficient permissions.', 'lets-code-cookie-banner' ) ) );
		}

		$years = isset( $_POST['years'] ) ? (int) $_POST['years'] : -1;
		if ( $years < 0 ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Invalid parameter.', 'lets-code-cookie-banner' ) ) );
		}

		$deleted = LCCB_Consent_Manager::delete_old_logs( $years );
		wp_send_json_success( array( 'deleted' => $deleted ) );
	}

	/**
	 * Renders the main plugin settings page with tab navigation.
	 */
	public function render_page() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}
		$settings = get_option( 'lccb_settings', array() );
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'general';

		// Backward-compat: old slug 'cookies' redirects to 'scanner'.
		if ( 'cookies' === $tab ) {
			$tab = 'scanner';
		}
		?>
		<div class="wrap lccb-admin-wrap">
			<h1><?php esc_html_e( "Let's Code Cookie Banner", 'lets-code-cookie-banner' ); ?></h1>
			<nav class="nav-tab-wrapper">
				<?php
				$tabs = array(
					'general'    => __( 'General', 'lets-code-cookie-banner' ),
					'scanner'    => __( 'Cookie Scanner', 'lets-code-cookie-banner' ),
					'registry'   => __( 'Cookie Registry', 'lets-code-cookie-banner' ),
					'shortcodes' => __( 'Shortcodes', 'lets-code-cookie-banner' ),
					'logs'       => __( 'Consent Log', 'lets-code-cookie-banner' ),
				);
				$tabs = apply_filters( 'lccb_admin_tabs', $tabs );
				foreach ( $tabs as $slug => $label ) :
					$class = ( $tab === $slug ) ? 'nav-tab nav-tab-active' : 'nav-tab';
					$url   = admin_url( 'admin.php?page=lccb-settings&tab=' . $slug );
					?>
					<a href="<?php echo esc_url( $url ); ?>" class="<?php echo esc_attr( $class ); ?>">
						<?php echo esc_html( $label ); ?>
					</a>
				<?php endforeach; ?>
			</nav>

			<?php if ( 'logs' === $tab ) : ?>
				<?php $this->render_logs_tab(); ?>
			<?php elseif ( 'scanner' === $tab ) : ?>
				<?php $this->render_scanner_tab(); ?>
			<?php elseif ( 'registry' === $tab ) : ?>
				<?php $this->render_registry_tab(); ?>
			<?php elseif ( 'shortcodes' === $tab ) : ?>
				<?php $this->render_shortcodes_tab(); ?>
			<?php else : ?>
				<form method="post" action="options.php">
					<?php settings_fields( 'lccb_settings_group' ); ?>
					<?php if ( 'general' === $tab ) : ?>
						<?php $this->render_general_tab( $settings ); ?>
					<?php else : ?>
						<input type="hidden" name="lccb_settings[_tab]" value="<?php echo esc_attr( $tab ); ?>">
						<div class="lccb-tab-content">
							<?php do_action( 'lccb_settings_tab_content', $tab, $settings ); ?>
						</div>
					<?php endif; ?>
					<?php submit_button( __( 'Save Settings', 'lets-code-cookie-banner' ) ); ?>
				</form>
			<?php endif; ?>
		</div>
		<?php
	}

	/**
	 * Renders the General settings tab content.
	 *
	 * @param array $settings Current plugin settings.
	 */
	private function render_general_tab( $settings ) {
		?>
		<input type="hidden" name="lccb_settings[_tab]" value="general">
		<div class="lccb-tab-content">
			<h2><?php esc_html_e( 'General Settings', 'lets-code-cookie-banner' ); ?></h2>
			<table class="form-table" role="presentation">
				<tr>
					<th scope="row"><?php esc_html_e( 'Enable Banner', 'lets-code-cookie-banner' ); ?></th>
					<td>
						<label>
							<input type="checkbox" name="lccb_settings[banner_enabled]" value="1"
								<?php checked( $settings['banner_enabled'] ?? '1', '1' ); ?>>
							<?php esc_html_e( 'Show the banner to site visitors', 'lets-code-cookie-banner' ); ?>
						</label>
					</td>
				</tr>
				<tr>
					<th scope="row"><?php esc_html_e( 'Floating "Manage Cookies" Button', 'lets-code-cookie-banner' ); ?></th>
					<td>
						<label>
							<input type="checkbox" name="lccb_settings[show_fab_button]" value="1"
								<?php checked( $settings['show_fab_button'] ?? '1', '1' ); ?>>
							<?php esc_html_e( 'Show the floating button that allows visitors to reopen cookie preferences', 'lets-code-cookie-banner' ); ?>
						</label>
						<p class="description">
							<?php
							printf(
								/* translators: %s: shortcode */
								esc_html__( 'If unchecked, visitors will have no default way to reopen cookie preferences. Add the %s shortcode to a page or menu item to give them access.', 'lets-code-cookie-banner' ),
								'<code>[lccb_manage_cookies]</code>'
							);
							?>
						</p>
					</td>
				</tr>
				<tr>
					<th scope="row"><?php esc_html_e( 'Cookie Policy URL', 'lets-code-cookie-banner' ); ?></th>
					<td>
						<input type="url" name="lccb_settings[cookie_policy_url]"
							value="<?php echo esc_attr( $settings['cookie_policy_url'] ?? '' ); ?>"
							class="regular-text" placeholder="https://example.com/cookie-policy">
						<p class="description"><?php esc_html_e( 'Link to the full Cookie Policy page. It will be shown in the banner.', 'lets-code-cookie-banner' ); ?></p>
					</td>
				</tr>
				<tr>
					<th scope="row"><?php esc_html_e( 'Consent Log (GDPR)', 'lets-code-cookie-banner' ); ?></th>
					<td>
						<label>
							<input type="checkbox" name="lccb_settings[logging_enabled]" value="1"
								<?php checked( $settings['logging_enabled'] ?? '1', '1' ); ?>>
							<?php esc_html_e( 'Enable anonymous consent log (recommended)', 'lets-code-cookie-banner' ); ?>
						</label>
						<p class="description">
							<?php esc_html_e( 'Records date, time and choices without storing personal data or IP addresses. Useful for demonstrating GDPR compliance.', 'lets-code-cookie-banner' ); ?>
						</p>
					</td>
				</tr>
				<tr>
					<th scope="row"><?php esc_html_e( 'Uninstall', 'lets-code-cookie-banner' ); ?></th>
					<td>
						<label>
							<input type="checkbox" name="lccb_settings[delete_on_uninstall]" value="1"
								<?php checked( $settings['delete_on_uninstall'] ?? '0', '1' ); ?>>
							<?php esc_html_e( 'Delete all data (settings and consent log) when the plugin is uninstalled', 'lets-code-cookie-banner' ); ?>
						</label>
						<p class="description">
							<?php esc_html_e( 'If unchecked (recommended), data is preserved and restored if the plugin is reinstalled.', 'lets-code-cookie-banner' ); ?>
						</p>
					</td>
				</tr>
				<tr>
					<th scope="row"><?php esc_html_e( '"Powered by" Attribution', 'lets-code-cookie-banner' ); ?></th>
					<td>
						<label>
							<input type="checkbox" name="lccb_settings[show_powered_by]" value="1"
								<?php checked( $settings['show_powered_by'] ?? '0', '1' ); ?>>
							<?php esc_html_e( 'Show "Powered by Let\'s Code" in the cookie banner', 'lets-code-cookie-banner' ); ?>
						</label>
						<p class="description">
							<?php esc_html_e( 'Opt in to display a small attribution link at the bottom of the cookie banner.', 'lets-code-cookie-banner' ); ?>
						</p>
					</td>
				</tr>
			</table>
			<hr>
			<h2><?php esc_html_e( 'Global Cookie Database', 'lets-code-cookie-banner' ); ?></h2>
			<p class="description">
				<?php esc_html_e( 'Connect to the Let\'s Code global cookie database to enrich your cookie list automatically and contribute your own cookie definitions.', 'lets-code-cookie-banner' ); ?>
			</p>
			<?php
			$api_key = LCCB_API_Client::get_api_key();
			?>
			<table class="form-table" role="presentation">
				<tr>
					<th scope="row"><?php esc_html_e( 'Connection status', 'lets-code-cookie-banner' ); ?></th>
					<td>
						<?php if ( $api_key ) : ?>
							<span style="color:#46b450;font-weight:600;">&#10003; <?php esc_html_e( 'Connected', 'lets-code-cookie-banner' ); ?></span>
							<code style="margin-left:8px;color:#646970;">
								<?php echo esc_html( substr( $api_key, 0, 8 ) . '...' . substr( $api_key, -4 ) ); ?>
							</code>
						<?php else : ?>
							<span style="color:#dc3232;font-weight:600;">&#10007; <?php esc_html_e( 'Not connected', 'lets-code-cookie-banner' ); ?></span>
							<p class="description" style="margin-top:4px;">
								<?php esc_html_e( 'Click the button below to register this site and generate an API key.', 'lets-code-cookie-banner' ); ?>
							</p>
						<?php endif; ?>
					</td>
				</tr>
			</table>
			<button type="button" id="lccb-api-reconnect" class="button button-secondary">
				<?php
				echo esc_html(
					$api_key
						? __( 'Reconnect', 'lets-code-cookie-banner' )
						: __( 'Connect', 'lets-code-cookie-banner' )
				);
				?>
			</button>
			<span id="lccb-api-reconnect-msg" class="lccb-cookie-msg" style="margin-left:8px;"></span>
		</div>
		<?php
	}

	/**
	 * Renders the Cookie Scanner tab.
	 */
	private function render_scanner_tab() {
		$detected         = LCCB_Cookie_Scanner::get_detected();
		$detected_scripts = LCCB_Cookie_Scanner::get_detected_scripts();
		$last_sync_ts     = get_option( 'lccb_suggest_scripts_last_sync', null );
		$cat_labels       = array(
			'necessary'   => __( 'Necessary', 'lets-code-cookie-banner' ),
			'statistics'  => __( 'Statistics', 'lets-code-cookie-banner' ),
			'marketing'   => __( 'Marketing', 'lets-code-cookie-banner' ),
			'preferences' => __( 'Preferences', 'lets-code-cookie-banner' ),
		);

		// Build a quick lookup of names already in the registry for comparison.
		$registry_names = array();
		foreach ( LCCB_Cookie_Registry::get_all() as $c ) {
			if ( ! empty( $c['name'] ) ) {
				$registry_names[] = $c['name'];
			}
		}

		?>
		<div class="lccb-tab-content">

			<!-- ===== CLIENT-SIDE SCAN ===== -->
			<h2><?php esc_html_e( 'Cookie Scan (Client-Side)', 'lets-code-cookie-banner' ); ?></h2>
			<p class="description" style="margin-bottom:16px;">
				<?php esc_html_e( 'Opens a new window that automatically scrolls through the specified page and detects all cookies accessible via JavaScript, including lazily loaded ones. Detected cookies (excluding HttpOnly) will appear in the table below. To scan all pages of your site, use the Full Website Scan below.', 'lets-code-cookie-banner' ); ?>
			</p>

			<h3 style="margin-top:0;font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:#3c434a;">
				<?php esc_html_e( 'Single Scan', 'lets-code-cookie-banner' ); ?>
			</h3>
			<div class="lccb-scan-launcher" style="display:flex;align-items:flex-end;gap:10px;flex-wrap:wrap;margin-bottom:6px;">
				<div>
					<label for="lccb-scan-url" style="display:block;margin-bottom:4px;font-weight:600;">
						<?php esc_html_e( 'URL to scan', 'lets-code-cookie-banner' ); ?>
					</label>
					<input type="url" id="lccb-scan-url" class="regular-text"
						value="<?php echo esc_attr( home_url( '/' ) ); ?>"
						style="min-width:320px;">
				</div>
				<div>
					<button type="button" id="lccb-scan-start" class="button button-primary">
						<?php esc_html_e( 'Scan', 'lets-code-cookie-banner' ); ?>
					</button>
				</div>
			</div>
			<label style="display:flex;align-items:center;gap:6px;cursor:pointer;margin-bottom:6px;font-size:13px;">
				<input type="checkbox" id="lccb-scan-force" checked>
				<?php esc_html_e( 'Force acceptance of all cookies', 'lets-code-cookie-banner' ); ?>
			</label>
			<p id="lccb-scan-msg" class="lccb-cookie-msg" style="margin-top:4px;margin-bottom:20px;"></p>

			<?php $this->render_full_website_scan(); ?>

			<hr style="margin:0 0 24px;">

			<!-- ===== COOKIE SCAN (SERVER-SIDE) ===== -->
			<?php
			$third_party_domains = get_option( 'lccb_third_party_domains', array() );
			$third_party_scan_ts = get_option( 'lccb_third_party_domains_ts', null );
			?>
			<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;margin-bottom:12px;">
				<h2 style="margin:0;"><?php esc_html_e( 'Cookie Scan (Server-Side)', 'lets-code-cookie-banner' ); ?></h2>
				<?php if ( ! empty( $third_party_domains ) ) : ?>
				<button type="button" id="lccb-clear-third-party-domains" class="button button-secondary">
					<?php esc_html_e( 'Clear list', 'lets-code-cookie-banner' ); ?>
				</button>
				<?php endif; ?>
			</div>
			<p class="description" style="margin-bottom:<?php echo ! empty( $third_party_domains ) ? '10px' : '16px'; ?>;">
				<?php esc_html_e( 'The WordPress server makes HTTP requests to target URLs and inspects Set-Cookie response headers. This is the only way to detect HttpOnly cookies, which are invisible to JavaScript.', 'lets-code-cookie-banner' ); ?>
			</p>
			<?php if ( ! empty( $third_party_domains ) ) : ?>
				<ul class="lccb-domain-list" style="margin:0 0 16px;padding:0;list-style:none;display:flex;flex-wrap:wrap;gap:6px;">
					<?php foreach ( $third_party_domains as $domain ) : ?>
						<li style="background:#f0f0f1;border:1px solid #c3c4c7;border-radius:4px;padding:3px 10px;font-size:13px;font-family:monospace;">
							<?php echo esc_html( $domain ); ?>
						</li>
					<?php endforeach; ?>
				</ul>
			<?php endif; ?>

			<h3 style="margin-top:0;font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:#3c434a;">
				<?php esc_html_e( 'Single Scan', 'lets-code-cookie-banner' ); ?>
			</h3>
			<p class="description" style="margin-bottom:8px;">
				<?php esc_html_e( 'Enter any URL to inspect its Set-Cookie response headers. Use this when you suspect a page sets HttpOnly cookies that you need to document in your cookie policy.', 'lets-code-cookie-banner' ); ?>
			</p>
			<div class="lccb-scan-launcher" style="display:flex;align-items:flex-end;gap:10px;flex-wrap:wrap;margin-bottom:8px;">
				<div>
					<label for="lccb-server-scan-url" style="display:block;margin-bottom:4px;font-weight:600;">
						<?php esc_html_e( 'URL to scan', 'lets-code-cookie-banner' ); ?>
					</label>
					<input type="url" id="lccb-server-scan-url" class="regular-text"
						placeholder="<?php esc_attr_e( 'Scan third party domain', 'lets-code-cookie-banner' ); ?>"
						style="min-width:320px;">
				</div>
				<div style="display:flex;flex-direction:column;gap:8px;">
					<button type="button" id="lccb-server-scan-start" class="button button-primary">
						<?php esc_html_e( 'Scan', 'lets-code-cookie-banner' ); ?>
					</button>
					<div id="lccb-server-scan-loader" style="display:none;align-items:center;gap:8px;">
						<span class="spinner" style="float:none;margin:0;visibility:visible;"></span>
						<span style="font-style:italic;color:#646970;"><?php esc_html_e( 'Scanning…', 'lets-code-cookie-banner' ); ?></span>
					</div>
				</div>
			</div>
			<p id="lccb-server-scan-msg" class="lccb-cookie-msg" style="margin-top:8px;"></p>
			<p class="description" style="margin-bottom:20px;color:#646970;">
				<?php esc_html_e( 'Note: Cookies set only after login or user interaction may not be detected.', 'lets-code-cookie-banner' ); ?>
			</p>

			<?php $this->render_bulk_third_party_scan( $third_party_domains, $third_party_scan_ts ); ?>

			<hr style="margin:0 0 24px;">

			<!-- ===== COOKIES FOUND BY SCANS ===== -->
			<?php
			$new_detected = array_values(
				array_filter(
					$detected,
					function ( $d ) use ( $registry_names ) {
						return ! in_array( $d['name'], $registry_names, true );
					}
				)
			);
			usort(
				$new_detected,
				function ( $a, $b ) {
					return strcmp( $a['name'], $b['name'] );
				}
			);
			?>
			<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;margin-bottom:12px;">
				<h2 style="margin:0;"><?php esc_html_e( 'Cookies Found by Scans', 'lets-code-cookie-banner' ); ?></h2>
				<?php if ( ! empty( $detected ) ) : ?>
				<button type="button" id="lccb-clear-detected" class="button button-secondary">
					<?php esc_html_e( 'Clear detections', 'lets-code-cookie-banner' ); ?>
				</button>
				<?php endif; ?>
			</div>
			<p class="description">
				<?php esc_html_e( 'Cookies detected by the client-side scan, server-side scan, and third-party domain scan that are not yet in the Cookie Registry. Cookies must be added to the registry to appear in your cookie policy. Use the Edit button to review and fill in the details, then Add to register them.', 'lets-code-cookie-banner' ); ?>
			</p>

			<?php if ( empty( $new_detected ) ) : ?>
				<div class="notice notice-info inline" style="margin:0 0 24px;">
					<p><?php esc_html_e( 'No new cookies detected. All detected cookies are already in the registry, or no scan has been run yet.', 'lets-code-cookie-banner' ); ?></p>
				</div>
			<?php else : ?>
			<table class="widefat striped lccb-cookie-registry-table" style="margin-bottom:24px;">
				<thead>
					<tr>
						<th><?php esc_html_e( 'Cookie Name', 'lets-code-cookie-banner' ); ?></th>
						<th><?php esc_html_e( 'Category', 'lets-code-cookie-banner' ); ?></th>
						<th><?php esc_html_e( 'Vendor', 'lets-code-cookie-banner' ); ?></th>
						<th><?php esc_html_e( 'Duration', 'lets-code-cookie-banner' ); ?></th>
						<th><?php esc_html_e( 'First Detected', 'lets-code-cookie-banner' ); ?></th>
						<th><?php esc_html_e( 'Actions', 'lets-code-cookie-banner' ); ?></th>
					</tr>
				</thead>
				<tbody>
				<?php foreach ( $new_detected as $d ) : ?>
				<tr data-name="<?php echo esc_attr( $d['name'] ); ?>"
					data-category="<?php echo esc_attr( $d['category'] ); ?>"
					data-vendor="<?php echo esc_attr( $d['vendor'] ); ?>"
					data-duration="<?php echo esc_attr( $d['duration'] ); ?>"
					data-description="<?php echo esc_attr( $d['description'] ); ?>">
					<td><code><?php echo esc_html( $d['name'] ); ?></code></td>
					<td><?php echo esc_html( $d['category'] ? ( $cat_labels[ $d['category'] ] ?? $d['category'] ) : '—' ); ?></td>
					<td><?php echo esc_html( ! empty( $d['vendor'] ) ? $d['vendor'] : '—' ); ?></td>
					<td><?php echo esc_html( ! empty( $d['duration'] ) ? $d['duration'] : '—' ); ?></td>
					<td><?php echo esc_html( $d['first_seen'] ); ?></td>
					<td class="lccb-cookie-actions">
						<button type="button" class="button button-small lccb-add-detected"
								data-name="<?php echo esc_attr( $d['name'] ); ?>"
								data-category="<?php echo esc_attr( $d['category'] ); ?>"
								data-vendor="<?php echo esc_attr( $d['vendor'] ); ?>"
								data-duration="<?php echo esc_attr( $d['duration'] ); ?>"
								data-description="<?php echo esc_attr( $d['description'] ); ?>">
							<?php esc_html_e( 'Add to Cookie Registry', 'lets-code-cookie-banner' ); ?>
						</button>
						<button type="button" class="button button-small lccb-edit-detected"
								data-name="<?php echo esc_attr( $d['name'] ); ?>"
								data-category="<?php echo esc_attr( $d['category'] ); ?>"
								data-vendor="<?php echo esc_attr( $d['vendor'] ); ?>"
								data-duration="<?php echo esc_attr( $d['duration'] ); ?>"
								data-description="<?php echo esc_attr( $d['description'] ); ?>">
							<?php esc_html_e( 'Edit', 'lets-code-cookie-banner' ); ?>
						</button>
					</td>
				</tr>
				<?php endforeach; ?>
				</tbody>
			</table>
			<?php endif; ?>

			<?php $this->render_add_all_button( $new_detected ); ?>

			<hr style="margin:24px 0;">

			<!-- ===== SUGGEST SCRIPTS TO COMMUNITY ===== -->
			<?php
			$suggest_scripts = array_values( $detected_scripts );
			usort(
				$suggest_scripts,
				function ( $a, $b ) {
					$a_synced = ! empty( $a['synced'] ) ? 1 : 0;
					$b_synced = ! empty( $b['synced'] ) ? 1 : 0;
					return $a_synced - $b_synced;
				}
			);
			?>
			<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;margin-bottom:12px;">
				<h2 style="margin:0;"><?php esc_html_e( 'Suggest Scripts to Community', 'lets-code-cookie-banner' ); ?></h2>
				<?php if ( ! empty( $suggest_scripts ) ) : ?>
				<button type="button" id="lccb-clear-detected-scripts" class="button button-secondary">
					<?php esc_html_e( 'Clear list', 'lets-code-cookie-banner' ); ?>
				</button>
				<?php endif; ?>
			</div>
			<p class="description">
				<?php esc_html_e( 'Help us improve the script-blocking database. After running a client-side scan, the list below shows all third-party scripts detected on your site. Clicking "Suggest to Community" sends only the script URL (scheme, domain, and path — no query parameters or personal data) to the Let\'s Code team for review. Your contribution may be used to improve automatic script recognition for everyone.', 'lets-code-cookie-banner' ); ?>
			</p>

			<?php if ( ! empty( $suggest_scripts ) ) : ?>
			<table class="widefat striped lccb-cookie-registry-table" id="lccb-suggest-scripts-table" style="margin-bottom:12px;">
				<thead>
					<tr>
						<th><?php esc_html_e( 'Script URL', 'lets-code-cookie-banner' ); ?></th>
						<th><?php esc_html_e( 'Domain', 'lets-code-cookie-banner' ); ?></th>
						<th><?php esc_html_e( 'Detected Cookies', 'lets-code-cookie-banner' ); ?></th>
						<th><?php esc_html_e( 'First Detected', 'lets-code-cookie-banner' ); ?></th>
						<th style="width:100px;text-align:center;"><?php esc_html_e( 'Synced', 'lets-code-cookie-banner' ); ?></th>
					</tr>
				</thead>
				<tbody>
				<?php foreach ( $suggest_scripts as $s ) : ?>
				<tr data-url="<?php echo esc_attr( $s['url'] ); ?>"
					data-synced="<?php echo ! empty( $s['synced'] ) ? '1' : '0'; ?>">
					<td><code style="word-break:break-all;font-size:12px;"><?php echo esc_html( $s['url'] ); ?></code></td>
					<td><?php echo esc_html( $s['domain'] ); ?></td>
					<td>
						<?php if ( ! empty( $s['cookies'] ) ) : ?>
						<code style="font-size:11px;"><?php echo esc_html( implode( ', ', $s['cookies'] ) ); ?></code>
						<?php else : ?>
						<span style="color:#ccc;">&#8212;</span>
						<?php endif; ?>
					</td>
					<td><?php echo esc_html( $s['first_seen'] ); ?></td>
					<td style="text-align:center;">
						<?php if ( ! empty( $s['synced'] ) ) : ?>
						<span class="lccb-synced-check" style="color:#2e7d32;font-size:18px;" title="<?php esc_attr_e( 'Synced', 'lets-code-cookie-banner' ); ?>">&#10003;</span>
						<?php else : ?>
						<span class="lccb-synced-check" style="color:#ccc;font-size:18px;" title="<?php esc_attr_e( 'Not yet synced', 'lets-code-cookie-banner' ); ?>">&#8212;</span>
						<?php endif; ?>
					</td>
				</tr>
				<?php endforeach; ?>
				</tbody>
			</table>
			<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:6px;">
				<?php if ( LCCB_API_Client::is_connected() ) : ?>
				<button type="button" id="lccb-suggest-scripts-btn" class="button button-primary">
					<?php esc_html_e( 'Suggest to Community', 'lets-code-cookie-banner' ); ?>
				</button>
				<?php else : ?>
				<p class="description" style="color:#d63638;margin:0;">
					<?php esc_html_e( 'Connect to the global database first (General Settings tab) to enable community contributions.', 'lets-code-cookie-banner' ); ?>
				</p>
				<?php endif; ?>
				<div id="lccb-suggest-scripts-loader" style="display:none;align-items:center;gap:8px;">
					<span class="spinner" style="float:none;margin:0;visibility:visible;"></span>
					<span style="font-style:italic;color:#646970;"><?php esc_html_e( 'Syncing…', 'lets-code-cookie-banner' ); ?></span>
				</div>
			</div>
			<p id="lccb-suggest-scripts-last-sync" style="font-size:12px;color:#646970;margin-bottom:4px;">
				<?php
				if ( $last_sync_ts ) {
					printf(
						/* translators: %s = date/time of last sync. */
						esc_html__( 'Last sync: %s', 'lets-code-cookie-banner' ),
						esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $last_sync_ts ) )
					);
				} else {
					esc_html_e( 'Never synced', 'lets-code-cookie-banner' );
				}
				?>
			</p>
			<p id="lccb-suggest-scripts-msg" class="lccb-cookie-msg" style="margin-top:4px;"></p>
			<?php else : ?>
			<div class="notice notice-info inline" style="margin:0 0 24px;">
				<p><?php esc_html_e( 'No third-party scripts detected yet. Run a client-side scan first to populate this list.', 'lets-code-cookie-banner' ); ?></p>
			</div>
			<?php endif; ?>

		</div>
		<?php $this->render_cookie_edit_modal(); ?>
		<?php
	}

	/**
	 * Renders the Cookie Registry tab.
	 */
	private function render_registry_tab() {
		$cookies    = LCCB_Cookie_Registry::get_all();
		$cat_labels = array(
			'necessary'   => __( 'Necessary', 'lets-code-cookie-banner' ),
			'statistics'  => __( 'Statistics', 'lets-code-cookie-banner' ),
			'marketing'   => __( 'Marketing', 'lets-code-cookie-banner' ),
			'preferences' => __( 'Preferences', 'lets-code-cookie-banner' ),
		);

		$incomplete_count = 0;
		foreach ( $cookies as $c ) {
			if ( empty( $c['name'] ) || empty( $c['vendor'] ) || empty( $c['description'] ) ) {
				++$incomplete_count;
			}
		}

		$lookup_ts   = get_option( 'lccb_api_lookup_ts', null );
		$lookup_date = $lookup_ts
			? esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $lookup_ts ) )
			: '';
		$has_unknown = (bool) get_option( 'lccb_api_lookup_has_unknown', false );

		?>
		<div class="lccb-tab-content">
			<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;margin-bottom:12px;">
				<h2 style="margin:0;"><?php esc_html_e( 'Cookie Registry', 'lets-code-cookie-banner' ); ?></h2>
				<div style="display:flex;gap:8px;flex-wrap:wrap;">
					<button type="button" id="lccb-cookie-add-new" class="button button-primary">
						<?php esc_html_e( 'Add Cookie', 'lets-code-cookie-banner' ); ?>
					</button>
					<button type="button" id="lccb-cookie-reset" class="button button-secondary">
						<?php esc_html_e( 'Clear Registry', 'lets-code-cookie-banner' ); ?>
					</button>
				</div>
			</div>
			<p class="description" style="margin-bottom:20px;">
				<?php esc_html_e( 'Official list of cookies used by the site. Displayed in the Preferences Panel and in the [lccb_cookie_policy] shortcode.', 'lets-code-cookie-banner' ); ?>
			</p>

			<?php if ( LCCB_API_Client::is_connected() ) : ?>
			<div style="border:1px solid #c3c4c7;border-radius:4px;padding:16px 20px;margin-bottom:24px;background:#f9f9f9;">
				<h3 style="font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:#3c434a;margin:0 0 8px;"><?php esc_html_e( 'Global Database Verification', 'lets-code-cookie-banner' ); ?></h3>
				<p class="description" style="margin-bottom:12px;">
					<?php esc_html_e( 'Use these tools to keep your registry in sync with the Let\'s Code global cookie database:', 'lets-code-cookie-banner' ); ?>
				</p>
				<ol class="description" style="margin:0 0 12px;padding-left:20px;">
					<li><?php esc_html_e( 'Click "Check unknown cookies in registry" to verify all cookies against the global database and automatically fill in missing details.', 'lets-code-cookie-banner' ); ?></li>
					<li><?php esc_html_e( 'Click "Send unknown cookies for check" to submit cookies not yet in the database. Their details will be reviewed and made available within a few days.', 'lets-code-cookie-banner' ); ?></li>
				</ol>
				<div style="display:flex;gap:8px;flex-wrap:wrap;">
					<?php if ( $incomplete_count > 0 ) : ?>
					<button type="button" id="lccb-api-lookup" class="button button-secondary">
						<?php
						printf(
							/* translators: %d = number of cookies with incomplete information */
							esc_html__( 'Check unknown cookies in registry (%d incomplete)', 'lets-code-cookie-banner' ),
							absint( $incomplete_count )
						);
						?>
					</button>
					<?php endif; ?>
					<button type="button" id="lccb-bulk-contribute" class="button button-secondary" style="<?php echo $has_unknown ? '' : 'display:none;'; ?>">
						<?php esc_html_e( 'Send unknown cookies for check', 'lets-code-cookie-banner' ); ?>
					</button>
				</div>
				<p id="lccb-api-lookup-msg" class="lccb-cookie-msg" style="margin-top:8px;margin-bottom:0;"></p>
				<p id="lccb-api-lookup-ts" style="font-size:12px;color:#646970;margin-top:6px;margin-bottom:0;<?php echo $lookup_date ? '' : 'display:none;'; ?>">
					<?php if ( $lookup_date ) : ?>
						<?php
						printf(
							/* translators: %s = date/time of the last database update. */
							esc_html__( 'Last update from global database: %s', 'lets-code-cookie-banner' ),
							$lookup_date // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped above
						);
						?>
					<?php endif; ?>
				</p>
			</div>
			<?php endif; ?>

			<!-- Registry filters -->
			<div id="lccb-registry-filters" style="display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap;margin-bottom:12px;">
				<div>
					<label for="lccb-filter-category" style="display:block;margin-bottom:4px;font-weight:600;font-size:13px;">
						<?php esc_html_e( 'Category', 'lets-code-cookie-banner' ); ?>
					</label>
					<select id="lccb-filter-category">
						<option value=""><?php esc_html_e( 'All categories', 'lets-code-cookie-banner' ); ?></option>
						<?php foreach ( $cat_labels as $slug => $label ) : ?>
						<option value="<?php echo esc_attr( $slug ); ?>"><?php echo esc_html( $label ); ?></option>
						<?php endforeach; ?>
					</select>
				</div>
				<div>
					<label for="lccb-filter-search" style="display:block;margin-bottom:4px;font-weight:600;font-size:13px;">
						<?php esc_html_e( 'Search (name, vendor, purpose)', 'lets-code-cookie-banner' ); ?>
					</label>
					<input type="search" id="lccb-filter-search" style="width:200px;"
							placeholder="<?php esc_attr_e( 'e.g. _ga, Google, statistics…', 'lets-code-cookie-banner' ); ?>">
				</div>
				<button type="button" id="lccb-filter-reset" class="button button-secondary">
					<?php esc_html_e( 'Reset filters', 'lets-code-cookie-banner' ); ?>
				</button>
				<span id="lccb-filter-count" style="font-size:13px;color:#646970;align-self:center;"></span>
			</div>

			<table id="lccb-registry-table" class="widefat striped lccb-cookie-registry-table" style="margin-bottom:24px;">
				<thead>
					<tr>
						<th><?php esc_html_e( 'Cookie Name', 'lets-code-cookie-banner' ); ?></th>
						<th><?php esc_html_e( 'Category', 'lets-code-cookie-banner' ); ?></th>
						<th><?php esc_html_e( 'Vendor', 'lets-code-cookie-banner' ); ?></th>
						<th><?php esc_html_e( 'Duration', 'lets-code-cookie-banner' ); ?></th>
						<th><?php esc_html_e( 'Purpose', 'lets-code-cookie-banner' ); ?></th>
						<th><?php esc_html_e( 'Status', 'lets-code-cookie-banner' ); ?></th>
						<th><?php esc_html_e( 'Actions', 'lets-code-cookie-banner' ); ?></th>
					</tr>
				</thead>
				<tbody>
				<?php if ( empty( $cookies ) ) : ?>
					<tr><td colspan="7"><?php esc_html_e( 'No cookies in the list.', 'lets-code-cookie-banner' ); ?></td></tr>
				<?php else : ?>
					<?php foreach ( $cookies as $c ) : ?>
						<?php
						$c_id     = $c['id'] ?? '';
						$c_source = $c['source'] ?? '';
						?>
					<tr data-id="<?php echo esc_attr( $c_id ); ?>"
						data-name="<?php echo esc_attr( $c['name'] ?? '' ); ?>"
						data-category="<?php echo esc_attr( $c['category'] ?? 'necessary' ); ?>"
						data-vendor="<?php echo esc_attr( $c['vendor'] ?? '' ); ?>"
						data-duration="<?php echo esc_attr( $c['duration'] ?? '' ); ?>"
						data-description="<?php echo esc_attr( $c['description'] ?? '' ); ?>"
						data-source="<?php echo esc_attr( $c_source ); ?>">
						<td><code><?php echo esc_html( $c['name'] ?? '' ); ?></code></td>
						<td><?php echo esc_html( $cat_labels[ $c['category'] ?? 'necessary' ] ?? $c['category'] ); ?></td>
						<td><?php echo esc_html( $c['vendor'] ?? '' ); ?></td>
						<td><?php echo esc_html( $c['duration'] ?? '' ); ?></td>
						<td><?php echo esc_html( $c['description'] ?? '' ); ?></td>
						<td>
							<?php if ( 'global_db' === $c_source ) : ?>
							<span style="color:#46b450;font-size:12px;font-weight:600;">&#10003; <?php esc_html_e( 'Checked', 'lets-code-cookie-banner' ); ?></span>
							<?php else : ?>
							<span style="color:#646970;font-size:12px;">&#8212; <?php esc_html_e( 'Not checked', 'lets-code-cookie-banner' ); ?></span>
							<?php endif; ?>
						</td>
						<td class="lccb-cookie-actions">
							<button type="button" class="button button-small lccb-cookie-edit"
									data-id="<?php echo esc_attr( $c_id ); ?>">
								<?php esc_html_e( 'Edit', 'lets-code-cookie-banner' ); ?>
							</button>
							<button type="button" class="button button-small lccb-cookie-delete"
									data-id="<?php echo esc_attr( $c_id ); ?>">
								<?php esc_html_e( 'Delete', 'lets-code-cookie-banner' ); ?>
							</button>
						</td>
					</tr>
					<?php endforeach; ?>
				<?php endif; ?>
				</tbody>
			</table>
		</div>
		<?php $this->render_cookie_edit_modal(); ?>
		<?php
	}

	/**
	 * Renders the Shortcodes tab.
	 */
	private function render_shortcodes_tab() {
		?>
		<div class="lccb-tab-content">
			<h2><?php esc_html_e( 'Shortcodes', 'lets-code-cookie-banner' ); ?></h2>
			<p class="description" style="margin-bottom:24px;">
				<?php esc_html_e( 'Use these shortcodes to embed cookie management elements anywhere on your site.', 'lets-code-cookie-banner' ); ?>
			</p>

			<div style="border:1px solid #c3c4c7;border-radius:4px;padding:20px 24px;margin-bottom:20px;background:#fff;">
				<h3 style="margin:0 0 6px;"><?php esc_html_e( 'Manage Cookies Button', 'lets-code-cookie-banner' ); ?></h3>
				<p class="description" style="margin:0 0 12px;">
					<?php esc_html_e( 'Renders a button that opens the cookie preferences panel. Use this on any page or widget to give visitors a way to review or change their cookie choices. Especially useful when the floating button is disabled in General Settings.', 'lets-code-cookie-banner' ); ?>
				</p>
				<code style="font-size:14px;padding:6px 10px;background:#f6f7f7;border:1px solid #dcdcde;border-radius:3px;display:inline-block;">[lccb_manage_cookies]</code>
				<p class="description" style="margin:8px 0 0;"><?php esc_html_e( 'Optional attribute: class="my-css-class"', 'lets-code-cookie-banner' ); ?></p>
			</div>

			<div style="border:1px solid #c3c4c7;border-radius:4px;padding:20px 24px;background:#fff;">
				<h3 style="margin:0 0 6px;"><?php esc_html_e( 'Cookie Policy Table', 'lets-code-cookie-banner' ); ?></h3>
				<p class="description" style="margin:0 0 12px;">
					<?php esc_html_e( 'Renders a detailed table of all cookies in the registry, grouped by category. Add this to your Cookie Policy page to automatically display up-to-date cookie information. The table colours can be customised in the Design tab.', 'lets-code-cookie-banner' ); ?>
				</p>
				<code style="font-size:14px;padding:6px 10px;background:#f6f7f7;border:1px solid #dcdcde;border-radius:3px;display:inline-block;">[lccb_cookie_policy]</code>
			</div>
		</div>
		<?php
	}

	/**
	 * Renders the shared cookie edit/add modal dialog.
	 * Used by both the Cookie Scanner tab (adding detected cookies) and the Cookie Registry tab (editing registry cookies).
	 */
	private function render_cookie_edit_modal() {
		?>
		<div id="lccb-edit-detected-overlay"
			style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:100000;align-items:center;justify-content:center;">
			<div style="background:#fff;border-radius:8px;padding:28px 32px;max-width:560px;width:90%;max-height:90vh;overflow-y:auto;box-shadow:0 8px 32px rgba(0,0,0,.3);">
				<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;">
					<h3 id="lccb-edit-modal-title" style="margin:0;"><?php esc_html_e( 'Cookie', 'lets-code-cookie-banner' ); ?></h3>
					<button type="button" id="lccb-edit-detected-close" style="background:none;border:none;cursor:pointer;font-size:22px;line-height:1;color:#666;padding:4px 8px;" aria-label="<?php esc_attr_e( 'Close', 'lets-code-cookie-banner' ); ?>">&times;</button>
				</div>
				<input type="hidden" id="lccb-edit-modal-mode" value="add">
				<input type="hidden" id="lccb-edit-modal-id" value="">
				<table class="form-table" role="presentation" style="margin:0;">
					<tr>
						<th scope="row"><?php esc_html_e( 'Cookie Name', 'lets-code-cookie-banner' ); ?></th>
						<td>
							<code id="lccb-edit-detected-name-display" style="font-size:14px;"></code>
							<input type="text" id="lccb-edit-detected-name-input" class="regular-text"
									style="display:none;"
									placeholder="<?php esc_attr_e( 'e.g. _ga, _fbp', 'lets-code-cookie-banner' ); ?>">
							<input type="hidden" id="lccb-edit-detected-name">
						</td>
					</tr>
					<tr>
						<th scope="row">
							<label for="lccb-edit-detected-category"><?php esc_html_e( 'Category', 'lets-code-cookie-banner' ); ?></label>
						</th>
						<td>
							<select id="lccb-edit-detected-category">
								<option value="necessary"><?php esc_html_e( 'Necessary', 'lets-code-cookie-banner' ); ?></option>
								<option value="statistics"><?php esc_html_e( 'Statistics', 'lets-code-cookie-banner' ); ?></option>
								<option value="marketing"><?php esc_html_e( 'Marketing', 'lets-code-cookie-banner' ); ?></option>
								<option value="preferences"><?php esc_html_e( 'Preferences', 'lets-code-cookie-banner' ); ?></option>
							</select>
						</td>
					</tr>
					<tr>
						<th scope="row">
							<label for="lccb-edit-detected-vendor"><?php esc_html_e( 'Vendor', 'lets-code-cookie-banner' ); ?></label>
						</th>
						<td>
							<input type="text" id="lccb-edit-detected-vendor" class="regular-text" placeholder="e.g. Google LLC">
						</td>
					</tr>
					<tr>
						<th scope="row">
							<label for="lccb-edit-detected-duration"><?php esc_html_e( 'Duration', 'lets-code-cookie-banner' ); ?></label>
						</th>
						<td>
							<input type="text" id="lccb-edit-detected-duration" class="regular-text"
									placeholder="<?php esc_attr_e( 'e.g. Session, 1 year, 90 days', 'lets-code-cookie-banner' ); ?>">
						</td>
					</tr>
					<tr>
						<th scope="row">
							<label for="lccb-edit-detected-desc"><?php esc_html_e( 'Purpose', 'lets-code-cookie-banner' ); ?></label>
						</th>
						<td>
							<textarea id="lccb-edit-detected-desc" class="large-text" rows="3"
										placeholder="<?php esc_attr_e( 'Description of the cookie purpose...', 'lets-code-cookie-banner' ); ?>"></textarea>
						</td>
					</tr>
				</table>
				<div style="margin-top:24px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
					<button type="button" id="lccb-edit-detected-save" class="button button-primary">
						<?php esc_html_e( 'Save', 'lets-code-cookie-banner' ); ?>
					</button>
					<button type="button" id="lccb-edit-detected-cancel" class="button button-secondary">
						<?php esc_html_e( 'Cancel', 'lets-code-cookie-banner' ); ?>
					</button>
					<span id="lccb-edit-detected-msg" class="lccb-cookie-msg"></span>
				</div>
			</div>
		</div>
		<?php
	}

	// ---- AJAX: Cookie Registry -----------------------------------------------

	/**
	 * AJAX handler: saves (adds or updates) a single cookie in the registry.
	 */
	public function ajax_cookie_save() {
		check_ajax_referer( 'lccb_cookie_registry_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Insufficient permissions.' ) );
		}

		$id   = isset( $_POST['cookie_id'] ) ? sanitize_text_field( wp_unslash( $_POST['cookie_id'] ) ) : '';
		$data = array(
			'name'        => isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '',
			'category'    => isset( $_POST['category'] ) ? sanitize_key( $_POST['category'] ) : 'necessary',
			'vendor'      => isset( $_POST['vendor'] ) ? sanitize_text_field( wp_unslash( $_POST['vendor'] ) ) : '',
			'duration'    => isset( $_POST['duration'] ) ? sanitize_text_field( wp_unslash( $_POST['duration'] ) ) : '',
			'description' => isset( $_POST['description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['description'] ) ) : '',
		);

		if ( $id ) {
			LCCB_Cookie_Registry::update( $id, $data );
			wp_send_json_success( array( 'action' => 'updated' ) );
		} else {
			$new = LCCB_Cookie_Registry::add( $data );
			wp_send_json_success(
				array(
					'action' => 'added',
					'id'     => $new['id'],
				)
			);
		}
	}

	/**
	 * AJAX handler: removes a cookie from the registry by ID.
	 */
	public function ajax_cookie_delete() {
		check_ajax_referer( 'lccb_cookie_registry_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Insufficient permissions.' ) );
		}
		$id = isset( $_POST['cookie_id'] ) ? sanitize_text_field( wp_unslash( $_POST['cookie_id'] ) ) : '';
		if ( $id ) {
			LCCB_Cookie_Registry::delete( $id );
		}
		wp_send_json_success();
	}

	/**
	 * AJAX handler: clears all cookies from the registry.
	 */
	public function ajax_cookie_reset() {
		check_ajax_referer( 'lccb_cookie_registry_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Insufficient permissions.' ) );
		}
		LCCB_Cookie_Registry::reset_to_defaults();
		wp_send_json_success();
	}

	/**
	 * AJAX handler: adds a single detected cookie to the registry.
	 */
	public function ajax_cookie_add_detected() {
		check_ajax_referer( 'lccb_cookie_registry_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Insufficient permissions.' ) );
		}
		$data = array(
			'name'        => isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '',
			'category'    => isset( $_POST['category'] ) ? sanitize_key( $_POST['category'] ) : 'necessary',
			'vendor'      => isset( $_POST['vendor'] ) ? sanitize_text_field( wp_unslash( $_POST['vendor'] ) ) : '',
			'duration'    => isset( $_POST['duration'] ) ? sanitize_text_field( wp_unslash( $_POST['duration'] ) ) : '',
			'description' => isset( $_POST['description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['description'] ) ) : '',
		);
		if ( '' === $data['name'] ) {
			wp_send_json_error( array( 'message' => 'Missing cookie name.' ) );
		}
		LCCB_Cookie_Registry::add( $data );
		wp_send_json_success();
	}

	/**
	 * AJAX handler: clears all auto-detected cookies.
	 */
	public function ajax_clear_detected() {
		check_ajax_referer( 'lccb_cookie_registry_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Insufficient permissions.' ) );
		}
		LCCB_Cookie_Scanner::clear_detected();
		wp_send_json_success();
	}

	/**
	 * Renders the Consent Logs tab with pagination and export controls.
	 */
	private function render_logs_tab() {
		$settings = get_option( 'lccb_settings', array() );
		if ( empty( $settings['logging_enabled'] ) ) {
			echo '<div class="notice notice-warning inline"><p>' . esc_html__( 'The consent log is disabled. Enable it in the General tab.', 'lets-code-cookie-banner' ) . '</p></div>';
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$page       = isset( $_GET['paged'] ) ? max( 1, (int) $_GET['paged'] ) : 1;
		$per_page   = 20;
		$logs       = LCCB_Consent_Manager::get_logs( $per_page, $page );
		$total      = LCCB_Consent_Manager::count_logs();
		$pages      = (int) ceil( $total / $per_page );
		$export_url = wp_nonce_url( admin_url( 'admin-ajax.php?action=lccb_export_logs' ), 'lccb_export_logs_nonce', 'nonce' );
		?>
		<div class="lccb-tab-content">
			<h2><?php esc_html_e( 'Consent Log', 'lets-code-cookie-banner' ); ?></h2>
			<p>
				<?php
				printf(
					/* translators: %d = total number of recorded consents */
					esc_html__( 'Total recorded consents: %d', 'lets-code-cookie-banner' ),
					(int) $total
				);
				?>
			</p>
			<a href="<?php echo esc_url( $export_url ); ?>" class="button button-secondary">
				<?php esc_html_e( 'Export CSV', 'lets-code-cookie-banner' ); ?>
			</a>

			<div class="notice notice-warning inline" style="margin:16px 0 0;">
				<p>
					<strong><?php esc_html_e( 'We recommend keeping your consent records.', 'lets-code-cookie-banner' ); ?></strong>
					<?php esc_html_e( 'The consent log is your legal evidence of GDPR compliance: it proves that each visitor gave informed consent, when they did so, and for which cookie categories. Deleting records means losing the ability to demonstrate compliance in the event of an inspection or complaint. Use the cleanup tool below only if storage constraints make it strictly necessary, and prefer retaining at least the last 2–3 years of records.', 'lets-code-cookie-banner' ); ?>
				</p>
			</div>

			<div class="lccb-cleanup-box">
				<h3><?php esc_html_e( 'Log Cleanup', 'lets-code-cookie-banner' ); ?></h3>
				<p class="description"><?php esc_html_e( 'Delete consents older than:', 'lets-code-cookie-banner' ); ?></p>
				<div class="lccb-cleanup-row">
					<select id="lccb-delete-period">
						<option value="1"><?php esc_html_e( '1 year', 'lets-code-cookie-banner' ); ?></option>
						<option value="2"><?php esc_html_e( '2 years', 'lets-code-cookie-banner' ); ?></option>
						<option value="3"><?php esc_html_e( '3 years', 'lets-code-cookie-banner' ); ?></option>
						<option value="5"><?php esc_html_e( '5 years', 'lets-code-cookie-banner' ); ?></option>
						<option value="10"><?php esc_html_e( '10 years', 'lets-code-cookie-banner' ); ?></option>
						<option value="0"><?php esc_html_e( 'All', 'lets-code-cookie-banner' ); ?></option>
					</select>
					<button id="lccb-delete-btn" class="button button-secondary">
						<?php esc_html_e( 'Delete', 'lets-code-cookie-banner' ); ?>
					</button>
					<span id="lccb-delete-msg" class="lccb-delete-msg"></span>
				</div>
			</div>

			<?php if ( $pages > 1 ) : ?>
			<div class="tablenav top">
				<div class="tablenav-pages">
					<?php
					echo wp_kses_post(
						paginate_links(
							array(
								'base'    => add_query_arg( 'paged', '%#%' ),
								'format'  => '',
								'current' => $page,
								'total'   => $pages,
							)
						)
					);
					?>
				</div>
			</div>
			<?php endif; ?>

			<table class="widefat striped" style="margin-top:16px;">
				<thead>
					<tr>
						<th scope="col"><?php esc_html_e( 'ID', 'lets-code-cookie-banner' ); ?></th>
						<th scope="col"><?php esc_html_e( 'Consent ID', 'lets-code-cookie-banner' ); ?></th>
						<th scope="col"><?php esc_html_e( 'Date', 'lets-code-cookie-banner' ); ?></th>
						<th scope="col"><?php esc_html_e( 'Statistics', 'lets-code-cookie-banner' ); ?></th>
						<th scope="col"><?php esc_html_e( 'Marketing', 'lets-code-cookie-banner' ); ?></th>
						<th scope="col"><?php esc_html_e( 'Preferences', 'lets-code-cookie-banner' ); ?></th>
						<th scope="col"><?php esc_html_e( 'Action', 'lets-code-cookie-banner' ); ?></th>
					</tr>
				</thead>
				<tbody>
					<?php if ( empty( $logs ) ) : ?>
					<tr><td colspan="7"><?php esc_html_e( 'No consents recorded yet.', 'lets-code-cookie-banner' ); ?></td></tr>
					<?php else : ?>
						<?php foreach ( $logs as $log ) : ?>
					<tr>
						<td><?php echo (int) $log->id; ?></td>
						<td><code><?php echo esc_html( $log->consent_id ); ?></code></td>
						<td><?php echo esc_html( $log->consent_date ); ?></td>
						<td><?php echo $log->statistics ? '<span class="lccb-yes">&#10003;</span>' : '<span class="lccb-no">&#10007;</span>'; ?></td>
						<td><?php echo $log->marketing ? '<span class="lccb-yes">&#10003;</span>' : '<span class="lccb-no">&#10007;</span>'; ?></td>
						<td><?php echo $log->preferences ? '<span class="lccb-yes">&#10003;</span>' : '<span class="lccb-no">&#10007;</span>'; ?></td>
						<td><?php echo esc_html( $log->action ); ?></td>
					</tr>
					<?php endforeach; ?>
					<?php endif; ?>
				</tbody>
			</table>
		</div>
		<?php
	}

	/**
	 * AJAX handler: performs a server-side HTTP scan of the site to detect cookies.
	 */
	public function ajax_server_scan() {
		check_ajax_referer( 'lccb_server_scan_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Insufficient permissions.' ) );
		}

		$url = isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : home_url( '/' );

		if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
			wp_send_json_error( array( 'message' => 'Invalid URL.' ) );
		}

		if ( ! $this->is_safe_url( $url ) ) {
			wp_send_json_error( array( 'message' => 'URL not allowed.' ) );
		}

		$response = wp_remote_get(
			$url,
			array(
				'timeout'   => 30,
				'sslverify' => false,
				'headers'   => array(
					'User-Agent' => 'Mozilla/5.0 (compatible; WordPress Cookie Scanner)',
				),
			)
		);

		if ( is_wp_error( $response ) ) {
			wp_send_json_error( array( 'message' => $response->get_error_message() ) );
		}

		$headers     = wp_remote_retrieve_headers( $response );
		$set_cookies = array();

		if ( isset( $headers['set-cookie'] ) ) {
			$raw         = $headers['set-cookie'];
			$set_cookies = is_array( $raw ) ? $raw : array( $raw );
		}

		$names = array();
		foreach ( $set_cookies as $cookie_line ) {
			$parts    = explode( ';', $cookie_line );
			$name_val = trim( $parts[0] );
			$eq       = strpos( $name_val, '=' );
			$name     = false !== $eq ? trim( substr( $name_val, 0, $eq ) ) : trim( $name_val );
			if ( '' !== $name ) {
				$names[] = sanitize_text_field( $name );
			}
		}

		$names = array_values( array_unique( $names ) );

		if ( empty( $names ) ) {
			wp_send_json_success( array( 'count' => 0 ) );
			return;
		}

		LCCB_Cookie_Scanner::scan_and_store( $names );

		wp_send_json_success( array( 'count' => count( $names ) ) );
	}

	/**
	 * AJAX handler: performs server-side HTTP requests to third-party domains to detect their cookies.
	 * Accepts an optional 'domain' POST param for single-domain scans (Free plan).
	 * Bulk scan (no domain param) is gated behind Pro.
	 */
	public function ajax_scan_third_party() {
		check_ajax_referer( 'lccb_server_scan_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Insufficient permissions.' ) );
		}

		$single_domain = isset( $_POST['domain'] ) ? sanitize_text_field( wp_unslash( $_POST['domain'] ) ) : '';
		$domains       = get_option( 'lccb_third_party_domains', array() );

		if ( empty( $domains ) ) {
			wp_send_json_error( array( 'message' => 'No third-party domains saved. Run a client-side scan first.' ) );
		}

		if ( '' !== $single_domain ) {
			// Single-domain scan: validate the domain is in the known list.
			if ( ! in_array( $single_domain, $domains, true ) ) {
				wp_send_json_error( array( 'message' => esc_html__( 'Domain not found in the detected list.', 'lets-code-cookie-banner' ) ) );
			}
			$domains = array( $single_domain );
		}

		$total_cookies = 0;
		$scanned       = 0;
		$failed        = array();

		foreach ( $domains as $domain ) {
			$url = 'https://' . $domain . '/';

			if ( ! $this->is_safe_url( $url ) ) {
				$failed[] = $domain;
				continue;
			}

			$response = wp_remote_get(
				$url,
				array(
					'timeout'   => 15,
					'sslverify' => false,
					'headers'   => array(
						'User-Agent' => 'Mozilla/5.0 (compatible; WordPress Cookie Scanner)',
					),
				)
			);

			if ( is_wp_error( $response ) ) {
				$failed[] = $domain;
				continue;
			}

			$headers     = wp_remote_retrieve_headers( $response );
			$set_cookies = array();
			if ( isset( $headers['set-cookie'] ) ) {
				$raw         = $headers['set-cookie'];
				$set_cookies = is_array( $raw ) ? $raw : array( $raw );
			}

			$names = array();
			foreach ( $set_cookies as $cookie_line ) {
				$parts    = explode( ';', $cookie_line );
				$name_val = trim( $parts[0] );
				$eq       = strpos( $name_val, '=' );
				$name     = false !== $eq ? trim( substr( $name_val, 0, $eq ) ) : trim( $name_val );
				if ( '' !== $name ) {
					$names[] = sanitize_text_field( $name );
				}
			}

			$names = array_values( array_unique( $names ) );
			if ( ! empty( $names ) ) {
				LCCB_Cookie_Scanner::scan_and_store( $names );
				$total_cookies += count( $names );
			}

			++$scanned;
		}

		wp_send_json_success(
			array(
				'scanned' => $scanned,
				'cookies' => $total_cookies,
				'failed'  => $failed,
			)
		);
	}

	/**
	 * AJAX handler: clears the list of detected third-party domains.
	 */
	public function ajax_clear_third_party_domains() {
		check_ajax_referer( 'lccb_server_scan_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Insufficient permissions.' ) );
		}

		delete_option( 'lccb_third_party_domains' );
		delete_option( 'lccb_third_party_domains_ts' );
		wp_send_json_success();
	}

	// ---- AJAX: Backoffice API integration -------------------------------------

	/**
	 * AJAX handler: re-registers this installation with the backoffice to obtain an API key.
	 */
	public function ajax_api_reconnect() {
		check_ajax_referer( 'lccb_api_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Insufficient permissions.', 'lets-code-cookie-banner' ) ) );
		}

		$ok = LCCB_API_Client::register();

		if ( $ok ) {
			$key = LCCB_API_Client::get_api_key();
			wp_send_json_success(
				array(
					'message'  => esc_html__( 'Connected successfully.', 'lets-code-cookie-banner' ),
					'key_hint' => esc_html( substr( $key, 0, 8 ) . '...' . substr( $key, -4 ) ),
				)
			);
		} else {
			wp_send_json_error(
				array(
					'message' => esc_html__( 'The service is temporarily unavailable. Please try again later or contact support@let-scode.it', 'lets-code-cookie-banner' ),
				)
			);
		}
	}

	/**
	 * AJAX handler: looks up incomplete registry cookies in the global database and updates them.
	 */
	public function ajax_api_lookup() {
		check_ajax_referer( 'lccb_api_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Insufficient permissions.', 'lets-code-cookie-banner' ) ) );
		}

		$all_cookies = LCCB_Cookie_Registry::get_all();

		// Collect names of cookies with missing info that didn't come from the global DB.
		$to_lookup = array();
		foreach ( $all_cookies as $c ) {
			if ( ! empty( $c['name'] ) && ( empty( $c['vendor'] ) || empty( $c['description'] ) ) ) {
				$to_lookup[ $c['id'] ] = $c['name'];
			}
		}

		if ( empty( $to_lookup ) ) {
			update_option( 'lccb_api_lookup_has_unknown', false );
			wp_send_json_success(
				array(
					'updated'   => 0,
					'remaining' => 0,
				)
			);
		}

		// Cap at 50 per API spec.
		$to_lookup = array_slice( $to_lookup, 0, 50, true );
		$result    = LCCB_API_Client::lookup( array_values( $to_lookup ) );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		// Build a name→data map from the API response.
		$found_map = array();
		foreach ( $result['found'] as $cookie_data ) {
			if ( ! empty( $cookie_data['name'] ) ) {
				$found_map[ $cookie_data['name'] ] = $cookie_data;
			}
		}

		$updated = 0;
		foreach ( $to_lookup as $id => $name ) {
			if ( ! isset( $found_map[ $name ] ) ) {
				continue;
			}
			$api_data = $found_map[ $name ];

			$update = array(
				'source' => 'global_db',
			);
			if ( ! empty( $api_data['category'] ) ) {
				$update['category'] = sanitize_key( $api_data['category'] );
			}
			if ( ! empty( $api_data['vendor'] ) ) {
				$update['vendor'] = sanitize_text_field( (string) $api_data['vendor'] );
			}
			if ( ! empty( $api_data['duration'] ) ) {
				$update['duration'] = sanitize_text_field( (string) $api_data['duration'] );
			}
			if ( ! empty( $api_data['description'] ) ) {
				$update['description'] = sanitize_textarea_field( (string) $api_data['description'] );
			}
			if ( isset( $api_data['script_urls'] ) && is_array( $api_data['script_urls'] ) ) {
				$update['script_urls'] = array_values(
					array_filter( array_map( 'esc_url_raw', $api_data['script_urls'] ), 'strlen' )
				);
			}

			LCCB_Cookie_Registry::update( $id, $update );
			++$updated;
		}

		$remaining = count( $to_lookup ) - $updated;
		$ts        = time();
		update_option( 'lccb_api_lookup_ts', $ts );
		update_option( 'lccb_api_lookup_has_unknown', $remaining > 0 );
		wp_send_json_success(
			array(
				'updated'      => $updated,
				'remaining'    => $remaining,
				'ts_formatted' => wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $ts ),
			)
		);
	}

	/**
	 * AJAX handler: contributes a single registry cookie to the global database.
	 */
	public function ajax_api_contribute() {
		check_ajax_referer( 'lccb_api_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Insufficient permissions.', 'lets-code-cookie-banner' ) ) );
		}

		$id = isset( $_POST['cookie_id'] ) ? sanitize_text_field( wp_unslash( $_POST['cookie_id'] ) ) : '';
		if ( '' === $id ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Missing cookie ID.', 'lets-code-cookie-banner' ) ) );
		}

		$all_cookies = LCCB_Cookie_Registry::get_all();
		$target      = null;
		foreach ( $all_cookies as $c ) {
			if ( isset( $c['id'] ) && $c['id'] === $id ) {
				$target = $c;
				break;
			}
		}

		if ( null === $target ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Cookie not found.', 'lets-code-cookie-banner' ) ) );
		}

		if ( ( $target['source'] ?? '' ) === 'global_db' ) {
			wp_send_json_error( array( 'message' => esc_html__( 'This cookie already comes from the global database.', 'lets-code-cookie-banner' ) ) );
		}

		$cookie_payload = array(
			'name'        => $target['name'] ?? '',
			'category'    => $target['category'] ?? 'necessary',
			'vendor'      => $target['vendor'] ?? '',
			'duration'    => $target['duration'] ?? '',
			'description' => $target['description'] ?? '',
		);

		$result = LCCB_API_Client::contribute( array( $cookie_payload ) );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		wp_send_json_success();
	}

	/**
	 * AJAX handler: bulk-contributes all registry cookies not sourced from the global database.
	 */
	public function ajax_api_contribute_all() {
		check_ajax_referer( 'lccb_api_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Insufficient permissions.', 'lets-code-cookie-banner' ) ) );
		}

		$payloads = array();
		foreach ( LCCB_Cookie_Registry::get_all() as $c ) {
			if ( empty( $c['name'] ) ) {
				continue;
			}
			if ( ( $c['source'] ?? '' ) === 'global_db' ) {
				continue;
			}
			$payloads[] = array(
				'name'        => $c['name'],
				'category'    => $c['category'] ?? 'necessary',
				'vendor'      => $c['vendor'] ?? '',
				'duration'    => $c['duration'] ?? '',
				'description' => $c['description'] ?? '',
			);
		}

		if ( empty( $payloads ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'No unknown cookies to send.', 'lets-code-cookie-banner' ) ) );
		}

		$result = LCCB_API_Client::contribute( $payloads );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		wp_send_json_success( array( 'sent' => count( $payloads ) ) );
	}

	/**
	 * AJAX handler: contributes detected third-party script URLs to the global database.
	 */
	public function ajax_suggest_scripts() {
		check_ajax_referer( 'lccb_api_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Insufficient permissions.', 'lets-code-cookie-banner' ) ) );
		}

		$scripts  = LCCB_Cookie_Scanner::get_detected_scripts();
		$unsynced = array_values(
			array_filter(
				$scripts,
				function ( $s ) {
					return empty( $s['synced'] );
				}
			)
		);

		if ( empty( $unsynced ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'All detected scripts have already been synced.', 'lets-code-cookie-banner' ) ) );
		}

		$payloads = array_map(
			function ( $s ) {
				$item = array(
					'script_url' => $s['url'],
					'domain'     => $s['domain'],
				);
				if ( ! empty( $s['cookies'] ) ) {
					$item['detected_cookies'] = array_values( $s['cookies'] );
				}
				return $item;
			},
			$unsynced
		);

		$result = LCCB_API_Client::contribute_scripts( $payloads );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		$synced_urls = wp_list_pluck( $unsynced, 'url' );
		LCCB_Cookie_Scanner::mark_scripts_synced( $synced_urls );

		$ts = time();
		update_option( 'lccb_suggest_scripts_last_sync', $ts );

		wp_send_json_success(
			array(
				'synced'       => count( $synced_urls ),
				'ts_formatted' => wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $ts ),
				'synced_urls'  => $synced_urls,
			)
		);
	}

	/**
	 * AJAX handler: clears all detected third-party script URLs.
	 */
	public function ajax_clear_detected_scripts() {
		check_ajax_referer( 'lccb_api_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Insufficient permissions.', 'lets-code-cookie-banner' ) ) );
		}

		LCCB_Cookie_Scanner::clear_detected_scripts();
		delete_option( 'lccb_suggest_scripts_last_sync' );

		wp_send_json_success();
	}

	// ---- Full Website Scan -------------------------------------------------------

	/**
	 * Renders the Full Website Scan table in the Cookie Scanner tab.
	 */
	private function render_full_website_scan() {
		$scan_pages = $this->get_scan_pages();
		?>
		<h3 style="font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:#3c434a;">
			<?php esc_html_e( 'Full Website Scan', 'lets-code-cookie-banner' ); ?>
		</h3>
		<div style="margin-bottom:24px;">
			<p class="description" style="margin-bottom:12px;">
				<?php esc_html_e( 'Click "Scan" on any row to open that page in a new tab with the cookie scanner active. You can open multiple tabs simultaneously — the browser will not block them since each click is a direct user action.', 'lets-code-cookie-banner' ); ?>
			</p>
			<label style="display:inline-flex;align-items:center;gap:6px;cursor:pointer;margin-bottom:16px;font-size:13px;">
				<input type="checkbox" id="lccb-scan-page-force" checked>
				<?php esc_html_e( 'Force acceptance of all cookies', 'lets-code-cookie-banner' ); ?>
			</label>
			<table class="wp-list-table widefat fixed striped" id="lccb-scan-page-table">
				<thead>
					<tr>
						<th style="width:30%;"><?php esc_html_e( 'Title', 'lets-code-cookie-banner' ); ?></th>
						<th style="width:20%;"><?php esc_html_e( 'Type', 'lets-code-cookie-banner' ); ?></th>
						<th><?php esc_html_e( 'URL', 'lets-code-cookie-banner' ); ?></th>
						<th style="width:14%;"><?php esc_html_e( 'Last Scan', 'lets-code-cookie-banner' ); ?></th>
						<th style="width:80px;"></th>
					</tr>
				</thead>
				<tbody>
					<?php if ( empty( $scan_pages ) ) : ?>
					<tr><td colspan="5"><?php esc_html_e( 'No pages found to scan.', 'lets-code-cookie-banner' ); ?></td></tr>
					<?php else : ?>
						<?php foreach ( $scan_pages as $scan_page ) : ?>
					<tr>
						<td><?php echo esc_html( $scan_page['title'] ); ?></td>
						<td><?php echo esc_html( $scan_page['type'] ); ?></td>
						<td style="word-break:break-all;">
							<a href="<?php echo esc_url( $scan_page['url'] ); ?>" target="_blank"><?php echo esc_html( $scan_page['url'] ); ?></a>
						</td>
						<td class="lccb-last-scan">
							<?php echo $scan_page['last_scan'] ? esc_html( $scan_page['last_scan'] ) : esc_html__( 'Never', 'lets-code-cookie-banner' ); ?>
						</td>
						<td>
							<button type="button" class="button button-secondary lccb-scan-page-btn" data-url="<?php echo esc_attr( $scan_page['url'] ); ?>">
								<?php esc_html_e( 'Scan', 'lets-code-cookie-banner' ); ?>
							</button>
						</td>
					</tr>
					<?php endforeach; ?>
					<?php endif; ?>
				</tbody>
			</table>
		</div>
		<p id="lccb-scan-page-msg" class="lccb-cookie-msg" style="margin-top:4px;margin-bottom:20px;"></p>
		<?php
	}

	/**
	 * Renders the Full Third-Party Domains Scan section in the Cookie Scanner tab.
	 *
	 * @param array    $third_party_domains Detected third-party domains.
	 * @param int|null $third_party_scan_ts Unix timestamp of last bulk scan, or null.
	 */
	private function render_bulk_third_party_scan( $third_party_domains, $third_party_scan_ts ) {
		?>
		<h3 style="font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:#3c434a;">
			<?php esc_html_e( 'Full Third-Party Domains Scan', 'lets-code-cookie-banner' ); ?>
		</h3>
		<p class="description" style="margin-bottom:10px;">
			<?php esc_html_e( 'List of third-party domains detected during the last client-side scan. Run a server-side scan on all of them at once to identify their HttpOnly cookies.', 'lets-code-cookie-banner' ); ?>
		</p>
		<?php if ( $third_party_scan_ts ) : ?>
			<p class="description" style="margin-bottom:10px;">
				<strong><?php esc_html_e( 'Last scan:', 'lets-code-cookie-banner' ); ?></strong>
				<?php echo esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $third_party_scan_ts ) ); ?>
			</p>
		<?php endif; ?>
		<?php if ( ! empty( $third_party_domains ) ) : ?>
			<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px;">
				<button type="button" id="lccb-scan-third-party-start" class="button button-primary">
					<?php
					printf(
						/* translators: %d = number of domains */
						esc_html__( 'Scan all third-party domains (%d)', 'lets-code-cookie-banner' ),
						count( $third_party_domains )
					);
					?>
				</button>
				<div id="lccb-third-party-loader" style="display:none;align-items:center;gap:8px;">
					<span class="spinner" style="float:none;margin:0;visibility:visible;"></span>
					<span style="font-style:italic;color:#646970;"><?php esc_html_e( 'Scanning…', 'lets-code-cookie-banner' ); ?></span>
				</div>
			</div>
			<p id="lccb-third-party-msg" class="lccb-cookie-msg" style="margin-top:8px;"></p>
		<?php else : ?>
			<div class="notice notice-info inline" style="margin:0 0 16px;">
				<p><?php esc_html_e( 'No third-party domains detected yet. Run a client-side scan first to populate this list.', 'lets-code-cookie-banner' ); ?></p>
			</div>
		<?php endif; ?>
		<?php
	}

	/**
	 * Renders the "Add all to Cookie Registry" button after the detected cookies table.
	 *
	 * @param array $new_detected Detected cookies not yet in the registry.
	 */
	private function render_add_all_button( $new_detected ) {
		if ( empty( $new_detected ) ) {
			return;
		}
		?>
		<div style="margin-bottom:24px;">
			<button type="button" id="lccb-add-all-detected" class="button button-primary">
				<?php esc_html_e( 'Add all to Cookie Registry', 'lets-code-cookie-banner' ); ?>
			</button>
			<span id="lccb-add-all-detected-msg" class="lccb-cookie-msg" style="margin-left:10px;"></span>
		</div>
		<?php
	}

	/**
	 * AJAX handler: records the timestamp of a manual Full Website Scan for the given URL.
	 */
	public function ajax_record_page_scan() {
		check_ajax_referer( 'lccb_record_page_scan_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Insufficient permissions.', 'lets-code-cookie-banner' ) ) );
		}
		$url = isset( $_POST['url'] ) ? esc_url_raw( sanitize_text_field( wp_unslash( $_POST['url'] ) ) ) : '';
		if ( ! $url ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Invalid URL.', 'lets-code-cookie-banner' ) ) );
		}
		$norm                = rtrim( $url, '/' );
		$timestamps          = get_option( 'lccb_page_scan_timestamps', array() );
		$now                 = current_time( 'mysql' );
		$timestamps[ $norm ] = $now;
		update_option( 'lccb_page_scan_timestamps', $timestamps );
		wp_send_json_success( array( 'timestamp' => $now ) );
	}

	/**
	 * AJAX handler: adds all detected cookies not yet in the registry.
	 */
	public function ajax_cookie_add_all_detected() {
		check_ajax_referer( 'lccb_cookie_registry_nonce', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Insufficient permissions.', 'lets-code-cookie-banner' ) ) );
		}

		$detected       = LCCB_Cookie_Scanner::get_detected();
		$registry_names = array();
		foreach ( LCCB_Cookie_Registry::get_all() as $c ) {
			if ( ! empty( $c['name'] ) ) {
				$registry_names[] = $c['name'];
			}
		}

		$added = 0;
		foreach ( $detected as $d ) {
			if ( ! in_array( $d['name'], $registry_names, true ) ) {
				LCCB_Cookie_Registry::add(
					array(
						'name'        => $d['name'],
						'category'    => $d['category'] ?? 'necessary',
						'vendor'      => $d['vendor'] ?? '',
						'duration'    => $d['duration'] ?? '',
						'description' => $d['description'] ?? '',
					)
				);
				++$added;
			}
		}

		wp_send_json_success( array( 'added' => $added ) );
	}

	/**
	 * Collects one representative page per WordPress template type for the scan table.
	 *
	 * @return array[]
	 */
	private function get_scan_pages() {
		$pages      = array();
		$added      = array();
		$timestamps = get_option( 'lccb_page_scan_timestamps', array() );

		$front_page_id = (int) get_option( 'page_on_front' );
		$front_title   = $front_page_id > 0 ? get_the_title( $front_page_id ) : get_bloginfo( 'name' );
		$this->maybe_add_scan_page(
			home_url( '/' ),
			$front_title ? $front_title : __( 'Home', 'lets-code-cookie-banner' ),
			__( 'Front Page', 'lets-code-cookie-banner' ),
			$pages,
			$added,
			$timestamps
		);

		if ( 'page' === get_option( 'show_on_front' ) ) {
			$blog_id = (int) get_option( 'page_for_posts' );
			if ( $blog_id > 0 ) {
				$this->maybe_add_scan_page(
					get_permalink( $blog_id ),
					get_the_title( $blog_id ),
					__( 'Blog Index', 'lets-code-cookie-banner' ),
					$pages,
					$added,
					$timestamps
				);
			}
		}

		$post_types = get_post_types( array( 'public' => true ), 'objects' );
		foreach ( $post_types as $post_type_obj ) {
			$posts = get_posts(
				array(
					'post_type'   => $post_type_obj->name,
					'post_status' => 'publish',
					'numberposts' => 1,
					'orderby'     => 'date',
					'order'       => 'DESC',
				)
			);
			if ( ! empty( $posts ) ) {
				$this->maybe_add_scan_page(
					get_permalink( $posts[0] ),
					$posts[0]->post_title,
					sprintf(
						/* translators: %s: post type singular label */
						__( 'Single (%s)', 'lets-code-cookie-banner' ),
						$post_type_obj->labels->singular_name
					),
					$pages,
					$added,
					$timestamps
				);
			}
			$archive = get_post_type_archive_link( $post_type_obj->name );
			if ( $archive ) {
				$this->maybe_add_scan_page(
					$archive,
					$post_type_obj->labels->name,
					sprintf(
						/* translators: %s: post type plural label */
						__( 'Archive (%s)', 'lets-code-cookie-banner' ),
						$post_type_obj->labels->name
					),
					$pages,
					$added,
					$timestamps
				);
			}
		}

		$taxonomies = get_taxonomies( array( 'public' => true ), 'objects' );
		foreach ( $taxonomies as $tax_obj ) {
			$terms = get_terms(
				array(
					'taxonomy'   => $tax_obj->name,
					'number'     => 1,
					'hide_empty' => true,
				)
			);
			if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
				$this->maybe_add_scan_page(
					get_term_link( $terms[0] ),
					$terms[0]->name,
					sprintf(
						/* translators: %s: taxonomy singular label */
						__( 'Term (%s)', 'lets-code-cookie-banner' ),
						$tax_obj->labels->singular_name
					),
					$pages,
					$added,
					$timestamps
				);
			}
		}

		$authors = get_users(
			array(
				'has_published_posts' => true,
				'number'              => 1,
			)
		);
		if ( ! empty( $authors ) ) {
			$this->maybe_add_scan_page(
				get_author_posts_url( $authors[0]->ID ),
				$authors[0]->display_name,
				__( 'Author Archive', 'lets-code-cookie-banner' ),
				$pages,
				$added,
				$timestamps
			);
		}

		return $pages;
	}

	/**
	 * Adds a page entry to $pages if $url is valid and not already in $added.
	 *
	 * @param  mixed  $url        URL string or WP_Error.
	 * @param  string $title      Human-readable page title.
	 * @param  string $type       Template type label.
	 * @param  array  $pages      Accumulated page list (by reference).
	 * @param  array  $added      Deduplication map keyed by normalised URL (by reference).
	 * @param  array  $timestamps Map of normalised URL to last-scan datetime string.
	 * @return void
	 */
	private function maybe_add_scan_page( $url, $title, $type, array &$pages, array &$added, array $timestamps ) {
		if ( ! $url || is_wp_error( $url ) ) {
			return;
		}
		$norm = rtrim( (string) $url, '/' );
		if ( isset( $added[ $norm ] ) ) {
			return;
		}
		$added[ $norm ] = true;
		$pages[]        = array(
			'url'       => (string) $url,
			'title'     => (string) $title,
			'type'      => (string) $type,
			'last_scan' => isset( $timestamps[ $norm ] ) ? $timestamps[ $norm ] : null,
		);
	}

	// ---- SSRF protection -------------------------------------------------------

	/**
	 * Returns true only if the URL scheme is http/https and the resolved IP
	 * is not in a private or reserved range (prevents SSRF attacks).
	 *
	 * @param string $url URL to validate.
	 * @return bool
	 */
	private function is_safe_url( $url ) {
		$parsed = wp_parse_url( $url );
		if ( ! $parsed || ! in_array( $parsed['scheme'] ?? '', array( 'http', 'https' ), true ) ) {
			return false;
		}
		$host = $parsed['host'] ?? '';
		if ( '' === $host ) {
			return false;
		}
		// Reject literal IPs in private/reserved ranges immediately.
		if ( filter_var( $host, FILTER_VALIDATE_IP ) ) {
			return (bool) filter_var( $host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE );
		}
		// Resolve hostname and check the resulting IP.
		$ip = gethostbyname( $host );
		// gethostbyname() returns the original string on DNS failure — allow wp_remote_get to handle it.
		if ( $ip === $host ) {
			return true;
		}
		return (bool) filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE );
	}
}
