<?php
/**
 * Admin Settings class
 *
 * @package WebEquipe\PDFSearch
 * @since 1.0.0
 */

namespace WebEquipe\PDFSearch;

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

/**
 * Admin Settings class
 */
class Admin_Settings {

	/**
	 * Initialize hooks
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		add_action( 'admin_menu', array( $this, 'add_settings_page' ) );
		add_action( 'admin_menu', array( $this, 'style_upgrade_submenu_label' ), 999 );
		add_action( 'admin_init', array( $this, 'register_settings' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
		add_action( 'wp_ajax_webequipe_pdf_reindex_ajax', array( $this, 'ajax_reindex' ) );
		add_action( 'wp_ajax_webequipe_pdf_reindex_single', array( $this, 'ajax_reindex_single' ) );
		add_action( 'wp_ajax_webequipe_pdf_unindex_single', array( $this, 'ajax_unindex_single' ) );
		add_action( 'wp_ajax_webequipe_pdf_exclude_single', array( $this, 'ajax_exclude_single' ) );
		add_action( 'wp_ajax_webequipe_pdf_include_single', array( $this, 'ajax_include_single' ) );
		add_action( 'wp_ajax_webequipe_pdf_clear_index', array( $this, 'ajax_clear_index' ) );
		add_action( 'wp_ajax_webequipe_pdf_scan_pdfs', array( $this, 'ajax_scan_pdfs' ) );
		add_action( 'wp_ajax_webequipe_pdf_bulk_action', array( $this, 'ajax_bulk_action' ) );
		add_action( 'wp_ajax_webequipe_pdf_cancel_indexing', array( $this, 'ajax_cancel_indexing' ) );
		add_action( 'wp_ajax_webequipe_pdf_resume_indexing', array( $this, 'ajax_resume_indexing' ) );
		add_action( 'wp_ajax_webequipe_pdf_activity_list', array( $this, 'ajax_activity_list' ) );
		add_action( 'wp_ajax_webequipe_pdf_export_activity', array( $this, 'ajax_export_activity' ) );
		add_filter( 'manage_media_columns', array( $this, 'add_media_column' ) );
		add_action( 'manage_media_custom_column', array( $this, 'display_media_column' ), 10, 2 );
		add_action( 'add_meta_boxes', array( $this, 'add_attachment_meta_box' ) );
		add_filter( 'bulk_actions-upload', array( $this, 'add_media_bulk_actions' ) );
		add_filter( 'handle_bulk_actions-upload', array( $this, 'handle_media_bulk_actions' ), 10, 3 );
		add_action( 'admin_notices', array( $this, 'bulk_action_admin_notices' ) );
		add_action( 'admin_notices', array( $this, 'maybe_show_legacy_index_notice' ) );
		add_filter( 'plugin_action_links_' . plugin_basename( WEBEQUIPE_PDF_SEARCH_DIR . 'webequipe-pdf-search.php' ), array( $this, 'add_plugin_action_links' ) );
	}

	/**
	 * Add settings page to admin menu
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function add_settings_page() {
		$icon_png_path = WEBEQUIPE_PDF_SEARCH_DIR . 'assets/icon.png';
		$icon_svg_path = WEBEQUIPE_PDF_SEARCH_DIR . 'assets/icon-pdf.svg';
		$icon_url      = 'dashicons-media-document';

		if ( file_exists( $icon_png_path ) ) {
			$icon_url = WEBEQUIPE_PDF_SEARCH_URL . 'assets/icon.png';
		} elseif ( file_exists( $icon_svg_path ) ) {
			$icon_svg = file_get_contents( $icon_svg_path );
			if ( $icon_svg ) {
				$icon_url = 'data:image/svg+xml;base64,' . base64_encode( $icon_svg );
			}
		}

		add_menu_page(
			__( 'WebEquipe PDF Search', 'webequipe-pdf-search' ),
			__( 'PDF Search', 'webequipe-pdf-search' ),
			'manage_options',
			'webequipe-pdf-search',
			array( $this, 'render_dashboard_page' ),
			$icon_url,
			25
		);

		add_submenu_page(
			'webequipe-pdf-search',
			__( 'WebEquipe PDF Search - Dashboard', 'webequipe-pdf-search' ),
			__( 'Dashboard', 'webequipe-pdf-search' ),
			'manage_options',
			'webequipe-pdf-search',
			array( $this, 'render_dashboard_page' )
		);

		add_submenu_page(
			'webequipe-pdf-search',
			__( 'WebEquipe PDF Search - Settings', 'webequipe-pdf-search' ),
			__( 'Settings', 'webequipe-pdf-search' ),
			'manage_options',
			'webequipe-pdf-search-settings',
			array( $this, 'render_settings_page' )
		);

		add_submenu_page(
			'webequipe-pdf-search',
			__( 'WebEquipe PDF Search - Manage PDFs', 'webequipe-pdf-search' ),
			__( 'Manage PDFs', 'webequipe-pdf-search' ),
			'manage_options',
			'webequipe-pdf-search-manage',
			array( $this, 'render_manage_page' )
		);

		add_submenu_page(
			'webequipe-pdf-search',
			__( 'WebEquipe PDF Search - Index Activity', 'webequipe-pdf-search' ),
			__( 'Index Activity', 'webequipe-pdf-search' ),
			'manage_options',
			'webequipe-pdf-search-activity',
			array( $this, 'render_activity_page' )
		);

		add_submenu_page(
			'webequipe-pdf-search',
			__( 'WebEquipe PDF Search - Help', 'webequipe-pdf-search' ),
			__( 'Help', 'webequipe-pdf-search' ),
			'manage_options',
			'webequipe-pdf-search-help',
			array( $this, 'render_help_page' )
		);

		add_submenu_page(
			'webequipe-pdf-search',
			__( 'PDF Search Pro Features', 'webequipe-pdf-search' ),
			__( 'Features', 'webequipe-pdf-search' ),
			'manage_options',
			'webequipe-pdf-search-upgrade',
			array( $this, 'render_upgrade_page' )
		);
	}

	/**
	 * Append highlighted Pro badge to the Features submenu label.
	 *
	 * @since 1.2.2
	 * @return void
	 */
	public function style_upgrade_submenu_label() {
		global $submenu;

		if ( ! isset( $submenu['webequipe-pdf-search'] ) ) {
			return;
		}

		foreach ( $submenu['webequipe-pdf-search'] as $index => $item ) {
			if ( isset( $item[2] ) && 'webequipe-pdf-search-upgrade' === $item[2] ) {
				$submenu['webequipe-pdf-search'][ $index ][0] .= ' <span class="webequipe-pdf-upgrade-menu-badge">Pro</span>';
				break;
			}
		}
	}

	/**
	 * Render dashboard page
	 *
	 * @since 1.1.0
	 * @return void
	 */
	public function render_dashboard_page() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		$dashboard_view = WEBEQUIPE_PDF_SEARCH_DIR . 'admin/views/dashboard-page.php';
		if ( file_exists( $dashboard_view ) ) {
			include $dashboard_view;
			return;
		}

		// Fallback for partial/older installations where the new view file is missing.
		$this->render_settings_page();
	}

	/**
	 * Render manage PDFs page
	 *
	 * @since 1.1.0
	 * @return void
	 */
	public function render_manage_page() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		$manage_view = WEBEQUIPE_PDF_SEARCH_DIR . 'admin/views/manage-page.php';
		if ( file_exists( $manage_view ) ) {
			include $manage_view;
			return;
		}

		// Fallback for partial/older installations where the new view file is missing.
		$this->render_settings_page();
	}

	/**
	 * Render index activity page
	 *
	 * @since 1.2.0
	 * @return void
	 */
	public function render_activity_page() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		$activity_view = WEBEQUIPE_PDF_SEARCH_DIR . 'admin/views/index-activity-page.php';
		if ( file_exists( $activity_view ) ) {
			include $activity_view;
			return;
		}

		$this->render_dashboard_page();
	}

	/**
	 * Render help page
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function render_help_page() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		include WEBEQUIPE_PDF_SEARCH_DIR . 'admin/views/help-page.php';
	}

	/**
	 * Render Pro Features page.
	 *
	 * @since 1.2.2
	 * @return void
	 */
	public function render_upgrade_page() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		include WEBEQUIPE_PDF_SEARCH_DIR . 'admin/views/upgrade-page.php';
	}

	/**
	 * Register plugin settings
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function register_settings() {
		register_setting(
			'webequipe_pdf_search_settings',
			'webequipe_pdf_search_settings',
			array(
				'sanitize_callback' => array( $this, 'sanitize_settings' ),
			)
		);

		$page = 'webequipe-pdf-search-settings';

		add_settings_section( 'webequipe_pdf_search_general', '', '__return_false', $page );
		add_settings_section( 'webequipe_pdf_search_indexing_method', '', '__return_false', $page );
		add_settings_section( 'webequipe_pdf_search_processing', '', '__return_false', $page );
		add_settings_section( 'webequipe_pdf_search_visibility', '', '__return_false', $page );
		add_settings_section( 'webequipe_pdf_search_display', '', '__return_false', $page );
		add_settings_section( 'webequipe_pdf_search_advanced', '', '__return_false', $page );
		add_settings_section( 'webequipe_pdf_search_danger', '', '__return_false', $page );

		// General Settings.
		add_settings_field(
			'enable_search',
			__( 'Enable Search Integration', 'webequipe-pdf-search' ),
			array( $this, 'render_checkbox_field' ),
			$page,
			'webequipe_pdf_search_general',
			array(
				'field'           => 'enable_search',
				'row_description' => __( 'Include PDFs in WordPress search results.', 'webequipe-pdf-search' ),
				'label'           => __( 'Include PDFs in WordPress search results', 'webequipe-pdf-search' ),
			)
		);

		add_settings_field(
			'indexing_method',
			'',
			array( $this, 'render_indexing_method_field' ),
			$page,
			'webequipe_pdf_search_indexing_method',
			array(
				'layout' => 'indexing_method',
			)
		);

		// Processing Settings.
		add_settings_field(
			'batch_size',
			__( 'Batch Size', 'webequipe-pdf-search' ),
			array( $this, 'render_number_field' ),
			$page,
			'webequipe_pdf_search_processing',
			array(
				'field'           => 'batch_size',
				'row_description' => __( 'Number of PDFs to process per batch.', 'webequipe-pdf-search' ),
				'min'             => 1,
				'max'             => 50,
				'step'            => 1,
				'control_hint'    => __( 'Recommended: 5–10 for shared hosting, 10–25 for stronger servers.', 'webequipe-pdf-search' ),
			)
		);

		add_settings_field(
			'max_file_size',
			__( 'Maximum File Size', 'webequipe-pdf-search' ),
			array( $this, 'render_number_field' ),
			$page,
			'webequipe_pdf_search_processing',
			array(
				'field'              => 'max_file_size',
				'row_description'    => __( 'Maximum PDF file size allowed for indexing (stored in bytes).', 'webequipe-pdf-search' ),
				'min'                => 1048576,
				'max'                => 524288000,
				'step'               => 1048576,
				'show_size_readable' => true,
				'control_hint'       => __( 'Default: 52,428,800 bytes (50 MB). Increase only if you index larger PDFs.', 'webequipe-pdf-search' ),
			)
		);

		add_settings_field(
			'processing_timeout',
			__( 'Processing Timeout', 'webequipe-pdf-search' ),
			array( $this, 'render_number_field' ),
			$page,
			'webequipe_pdf_search_processing',
			array(
				'field'           => 'processing_timeout',
				'row_description' => __( 'Maximum processing time for each PDF.', 'webequipe-pdf-search' ),
				'min'             => 10,
				'max'             => 300,
				'step'            => 10,
				'control_hint'    => __( 'Time in seconds. Increase only if large PDFs fail during processing.', 'webequipe-pdf-search' ),
			)
		);

		add_settings_field(
			'pages_per_batch',
			__( 'Pages Per Batch', 'webequipe-pdf-search' ),
			array( $this, 'render_number_field' ),
			$page,
			'webequipe_pdf_search_processing',
			array(
				'field'           => 'pages_per_batch',
				'row_description' => __( 'Pages indexed per background or cron step for large PDFs.', 'webequipe-pdf-search' ),
				'min'             => 1,
				'max'             => 50,
				'step'            => 1,
				'control_hint'    => __( 'Lower this if page-by-page indexing stops early or times out.', 'webequipe-pdf-search' ),
			)
		);

		add_settings_field(
			'page_index_threshold',
			__( 'Page Index Threshold', 'webequipe-pdf-search' ),
			array( $this, 'render_number_field' ),
			$page,
			'webequipe_pdf_search_processing',
			array(
				'field'           => 'page_index_threshold',
				'row_description' => __( 'PDFs with more pages than this use background page-by-page indexing.', 'webequipe-pdf-search' ),
				'min'             => 1,
				'max'             => 5000,
				'step'            => 1,
				'control_hint'    => __( 'Default: 50. Large manuals benefit from background mode.', 'webequipe-pdf-search' ),
			)
		);

		add_settings_field(
			'max_page_content_length',
			__( 'Max Page Content Length', 'webequipe-pdf-search' ),
			array( $this, 'render_number_field' ),
			$page,
			'webequipe_pdf_search_processing',
			array(
				'field'           => 'max_page_content_length',
				'row_description' => __( 'Maximum characters stored per page in the search index.', 'webequipe-pdf-search' ),
				'min'             => 0,
				'max'             => 10000000,
				'step'            => 1000,
				'control_hint'    => __( '0 = unlimited per page. Re-index after changing this value.', 'webequipe-pdf-search' ),
			)
		);

		// Search Visibility.
		add_settings_field(
			'default_pdf_visibility_pro',
			__( 'Default PDF Visibility', 'webequipe-pdf-search' ),
			array( $this, 'render_default_pdf_visibility_upsell' ),
			$page,
			'webequipe_pdf_search_visibility',
			array(
				'layout'          => 'pro_upsell',
				'row_description' => __( 'Choose whether newly indexed PDFs are public or private by default.', 'webequipe-pdf-search' ),
			)
		);

		add_settings_field(
			'excerpt_length',
			__( 'Search Result Excerpt Length', 'webequipe-pdf-search' ),
			array( $this, 'render_number_field' ),
			$page,
			'webequipe_pdf_search_visibility',
			array(
				'field'           => 'excerpt_length',
				'row_description' => __( 'Character length used for search result snippets.', 'webequipe-pdf-search' ),
				'min'             => 50,
				'max'             => 1000,
				'step'            => 10,
				'control_hint'    => __( 'Recommended: 160–240 characters.', 'webequipe-pdf-search' ),
			)
		);

		// Search Result Display (toggle layout).
		add_settings_field(
			'show_pdf_icon',
			__( 'PDF Icon', 'webequipe-pdf-search' ),
			array( $this, 'render_checkbox_field' ),
			$page,
			'webequipe_pdf_search_display',
			array(
				'field'           => 'show_pdf_icon',
				'layout'          => 'toggle',
				'row_description' => __( 'Display a PDF icon beside each result.', 'webequipe-pdf-search' ),
			)
		);

		add_settings_field(
			'show_file_size',
			__( 'File Size', 'webequipe-pdf-search' ),
			array( $this, 'render_checkbox_field' ),
			$page,
			'webequipe_pdf_search_display',
			array(
				'field'           => 'show_file_size',
				'layout'          => 'toggle',
				'row_description' => __( 'Show the PDF file size in result metadata.', 'webequipe-pdf-search' ),
			)
		);

		add_settings_field(
			'show_page_count',
			__( 'Number of Pages', 'webequipe-pdf-search' ),
			array( $this, 'render_checkbox_field' ),
			$page,
			'webequipe_pdf_search_display',
			array(
				'field'           => 'show_page_count',
				'layout'          => 'toggle',
				'row_description' => __( 'Show total page count for indexed PDFs.', 'webequipe-pdf-search' ),
			)
		);

		add_settings_field(
			'show_modified_date',
			__( 'Last Updated Date', 'webequipe-pdf-search' ),
			array( $this, 'render_checkbox_field' ),
			$page,
			'webequipe_pdf_search_display',
			array(
				'field'           => 'show_modified_date',
				'layout'          => 'toggle',
				'row_description' => __( 'Display the last updated date in search result metadata.', 'webequipe-pdf-search' ),
			)
		);

		add_settings_field(
			'show_author',
			__( 'Author', 'webequipe-pdf-search' ),
			array( $this, 'render_checkbox_field' ),
			$page,
			'webequipe_pdf_search_display',
			array(
				'field'           => 'show_author',
				'layout'          => 'toggle',
				'row_description' => __( 'Display uploader or author name in result metadata.', 'webequipe-pdf-search' ),
			)
		);

		add_settings_field(
			'show_preview',
			__( 'PDF Preview Thumbnail', 'webequipe-pdf-search' ),
			array( $this, 'render_checkbox_field' ),
			$page,
			'webequipe_pdf_search_display',
			array(
				'field'           => 'show_preview',
				'layout'          => 'toggle',
				'row_description' => __( 'Show a small PDF thumbnail preview in search results.', 'webequipe-pdf-search' ),
			)
		);

		add_settings_field(
			'show_summary',
			__( 'Summary Text', 'webequipe-pdf-search' ),
			array( $this, 'render_checkbox_field' ),
			$page,
			'webequipe_pdf_search_display',
			array(
				'field'           => 'show_summary',
				'layout'          => 'toggle',
				'row_description' => __( 'Display summary or snippet text below each result.', 'webequipe-pdf-search' ),
			)
		);

		// Advanced Settings.
		add_settings_field(
			'enable_debug',
			__( 'Debug Logging', 'webequipe-pdf-search' ),
			array( $this, 'render_checkbox_field' ),
			$page,
			'webequipe_pdf_search_advanced',
			array(
				'field'           => 'enable_debug',
				'row_description' => __( 'Log detailed debugging information for troubleshooting.', 'webequipe-pdf-search' ),
				'label'           => __( 'Enable debug logging', 'webequipe-pdf-search' ),
			)
		);

		add_settings_field(
			'memory_limit',
			__( 'Memory Limit', 'webequipe-pdf-search' ),
			array( $this, 'render_text_field' ),
			$page,
			'webequipe_pdf_search_advanced',
			array(
				'field'              => 'memory_limit',
				'row_description'    => __( 'PHP memory limit used during PDF processing.', 'webequipe-pdf-search' ),
				'show_server_memory' => true,
			)
		);

		add_settings_field(
			'enable_background',
			__( 'Background Processing', 'webequipe-pdf-search' ),
			array( $this, 'render_checkbox_field' ),
			$page,
			'webequipe_pdf_search_advanced',
			array(
				'field'           => 'enable_background',
				'row_description' => __( 'Process large PDFs in the background when possible.', 'webequipe-pdf-search' ),
				'label'           => __( 'Enable background processing', 'webequipe-pdf-search' ),
			)
		);

		// Danger Zone.
		add_settings_field(
			'delete_on_uninstall',
			__( 'Delete Data on Uninstall', 'webequipe-pdf-search' ),
			array( $this, 'render_checkbox_field' ),
			$page,
			'webequipe_pdf_search_danger',
			array(
				'field'           => 'delete_on_uninstall',
				'row_description' => __( 'Remove all plugin data when the plugin is uninstalled.', 'webequipe-pdf-search' ),
				'label'           => __( 'Remove all plugin data on uninstall', 'webequipe-pdf-search' ),
				'control_hint'    => __( 'This should stay disabled for most websites.', 'webequipe-pdf-search' ),
			)
		);
	}

	/**
	 * Sanitize settings
	 *
	 * @since 1.0.0
	 * @param array $input Raw settings input.
	 * @return array Sanitized settings
	 */
	public function sanitize_settings( $input ) {
		$sanitized = array();

		if ( isset( $input['indexing_method'] ) ) {
			$method                        = sanitize_key( $input['indexing_method'] );
			$sanitized['enable_indexing']  = ( 'native' === $method );
		} elseif ( isset( $input['enable_indexing'] ) ) {
			$sanitized['enable_indexing'] = (bool) $input['enable_indexing'];
		} else {
			$sanitized['enable_indexing'] = false;
		}

		// Boolean fields.
		$boolean_fields = array( 'enable_search', 'show_pdf_icon', 'show_file_size', 'show_page_count', 'show_modified_date', 'show_author', 'show_preview', 'show_summary', 'enable_debug', 'enable_background', 'delete_on_uninstall' );
		foreach ( $boolean_fields as $field ) {
			$sanitized[ $field ] = isset( $input[ $field ] ) ? (bool) $input[ $field ] : false;
		}

		// Number fields.
		$number_fields = array(
			'max_file_size'      => array(
				'min' => 1048576,
				'max' => 524288000,
			),
			'excerpt_length'     => array(
				'min' => 50,
				'max' => 1000,
			),
			'processing_timeout' => array(
				'min' => 10,
				'max' => 300,
			),
			'batch_size'              => array(
				'min' => 1,
				'max' => 50,
			),
			'pages_per_batch'         => array(
				'min' => 1,
				'max' => 50,
			),
			'page_index_threshold'    => array(
				'min' => 1,
				'max' => 5000,
			),
			'max_page_content_length' => array(
				'min' => 0,
				'max' => 10000000,
			),
		);

		foreach ( $number_fields as $field => $limits ) {
			$value               = isset( $input[ $field ] ) ? absint( $input[ $field ] ) : 0;
			$sanitized[ $field ] = max( $limits['min'], min( $limits['max'], $value ) );
		}

		// Text fields.
		$sanitized['memory_limit']     = isset( $input['memory_limit'] ) ? sanitize_text_field( $input['memory_limit'] ) : '256M';
		$sanitized['pdf_icon_type']    = isset( $input['pdf_icon_type'] ) ? sanitize_text_field( $input['pdf_icon_type'] ) : 'svg';
		$sanitized['css_class_prefix'] = isset( $input['css_class_prefix'] ) ? sanitize_text_field( $input['css_class_prefix'] ) : 'webequipe-pdf';
		$sanitized['cron_schedule']    = isset( $input['cron_schedule'] ) ? sanitize_text_field( $input['cron_schedule'] ) : 'hourly';

		return $sanitized;
	}

	/**
	 * Render settings page
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function render_settings_page() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		include WEBEQUIPE_PDF_SEARCH_DIR . 'admin/views/settings-page.php';
	}

	/**
	 * Render the Pro launch banner on plugin admin pages.
	 *
	 * @since 1.2.0
	 * @return void
	 */
	public static function render_pro_banner() {
		Pro_Launch_Banner::render();
	}

	/**
	 * Render all settings sections as Pro-style cards.
	 *
	 * @since 1.2.1
	 * @return void
	 */
	public static function render_settings_page_cards() {
		$cards = array(
			array(
				'id'       => 'webequipe_pdf_search_general',
				'title'    => __( 'General Settings', 'webequipe-pdf-search' ),
				'subtitle' => __( 'Control search integration for indexed PDFs.', 'webequipe-pdf-search' ),
				'danger'   => false,
			),
			array(
				'id'       => 'webequipe_pdf_search_indexing_method',
				'title'    => __( 'PDF Indexing Method', 'webequipe-pdf-search' ),
				'subtitle' => __( 'Choose how PDF content should be extracted and indexed for search.', 'webequipe-pdf-search' ),
				'danger'   => false,
			),
			array(
				'id'       => 'webequipe_pdf_search_processing',
				'title'    => __( 'Processing Settings', 'webequipe-pdf-search' ),
				'subtitle' => __( 'Control file limits and batch processing behavior.', 'webequipe-pdf-search' ),
				'danger'   => false,
			),
			array(
				'id'       => 'webequipe_pdf_search_visibility',
				'title'    => __( 'Search Visibility', 'webequipe-pdf-search' ),
				'subtitle' => __( 'Control where PDF results appear and who can find them.', 'webequipe-pdf-search' ),
				'danger'   => false,
			),
			array(
				'id'       => 'webequipe_pdf_search_display',
				'title'    => __( 'Search Result Display', 'webequipe-pdf-search' ),
				'subtitle' => __( 'Choose which metadata should appear in PDF search results.', 'webequipe-pdf-search' ),
				'danger'   => false,
			),
			array(
				'id'       => 'webequipe_pdf_search_advanced',
				'title'    => __( 'Advanced Settings', 'webequipe-pdf-search' ),
				'subtitle' => __( 'Technical settings for debugging and server processing.', 'webequipe-pdf-search' ),
				'danger'   => false,
			),
			array(
				'id'       => 'webequipe_pdf_search_danger',
				'title'    => __( 'Danger Zone', 'webequipe-pdf-search' ),
				'subtitle' => __( 'These settings can remove plugin data. Use carefully.', 'webequipe-pdf-search' ),
				'danger'   => true,
			),
		);

		foreach ( $cards as $card ) {
			self::render_settings_card(
				$card['id'],
				$card['title'],
				$card['subtitle'],
				$card['danger']
			);
		}
	}

	/**
	 * Render one settings card and its fields.
	 *
	 * @since 1.2.1
	 * @param string $section_id Settings section ID.
	 * @param string $title      Card title.
	 * @param string $subtitle   Card subtitle.
	 * @param bool   $is_danger  Whether this is the danger zone card.
	 * @return void
	 */
	public static function render_settings_card( $section_id, $title, $subtitle, $is_danger = false ) {
		global $wp_settings_fields;

		$page   = 'webequipe-pdf-search-settings';
		$fields = isset( $wp_settings_fields[ $page ][ $section_id ] ) ? $wp_settings_fields[ $page ][ $section_id ] : array();

		if ( empty( $fields ) ) {
			return;
		}

		$card_class = 'webequipe-pdf-settings-card';
		if ( $is_danger ) {
			$card_class .= ' webequipe-pdf-settings-card--danger';
		}

		echo '<div class="' . esc_attr( $card_class ) . '">';
		echo '<div class="webequipe-pdf-settings-card-header">';
		echo '<h2 class="webequipe-pdf-settings-card-title">' . esc_html( $title ) . '</h2>';
		if ( $subtitle ) {
			echo '<p class="webequipe-pdf-settings-card-subtitle">' . esc_html( $subtitle ) . '</p>';
		}
		echo '</div>';
		echo '<div class="webequipe-pdf-settings-card-body">';

		foreach ( $fields as $field ) {
			$args = isset( $field['args'] ) && is_array( $field['args'] ) ? $field['args'] : array();
			$row_description = isset( $args['row_description'] ) ? $args['row_description'] : '';
			$layout          = isset( $args['layout'] ) ? $args['layout'] : '';

			if ( 'indexing_method' === $layout ) {
				echo '<div class="webequipe-pdf-settings-row webequipe-pdf-settings-row--indexing-method">';
				echo '<div class="webequipe-pdf-settings-row-control webequipe-pdf-settings-row-control--full">';
				call_user_func( $field['callback'], $args );
				echo '</div>';
				echo '</div>';
				continue;
			}

			$row_class = 'webequipe-pdf-settings-row';
			if ( 'toggle' === $layout ) {
				$row_class .= ' webequipe-pdf-settings-row--toggle';
			}
			if ( 'pro_upsell' === $layout ) {
				$row_class .= ' webequipe-pdf-settings-row--pro-upsell';
			}

			echo '<div class="' . esc_attr( $row_class ) . '">';
			echo '<div class="webequipe-pdf-settings-row-main">';
			if ( $field['title'] ) {
				echo '<div class="webequipe-pdf-settings-row-label">';
				echo esc_html( $field['title'] );
				if ( 'pro_upsell' === $layout ) {
					echo ' <span class="webequipe-pdf-indexing-method-badge webequipe-pdf-indexing-method-badge--pro">' . esc_html__( 'Available on Pro', 'webequipe-pdf-search' ) . '</span>';
				}
				echo '</div>';
			}
			if ( $row_description ) {
				echo '<p class="webequipe-pdf-settings-row-desc">' . esc_html( $row_description ) . '</p>';
			}
			echo '</div>';
			echo '<div class="webequipe-pdf-settings-row-control">';
			call_user_func( $field['callback'], $args );
			echo '</div>';
			echo '</div>';
		}

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

	/**
	 * Render disabled Default PDF Visibility upsell (Pro feature preview).
	 *
	 * @since 1.2.2
	 * @param array $args Field arguments.
	 * @return void
	 */
	public function render_default_pdf_visibility_upsell( $args = array() ) {
		unset( $args );
		$upgrade_url = Pro_Launch_Banner::get_admin_pricing_url();
		?>
		<div class="webequipe-pdf-settings-pro-upsell-control">
			<label class="webequipe-pdf-settings-checkbox-control webequipe-pdf-settings-checkbox-control--disabled">
				<input type="checkbox" disabled="disabled">
				<?php esc_html_e( 'Make new PDFs private by default', 'webequipe-pdf-search' ); ?>
			</label>
			<p class="webequipe-pdf-settings-control-hint">
				<?php esc_html_e( 'Private PDFs can only be found by signed-in users with permission.', 'webequipe-pdf-search' ); ?>
			</p>
			<a class="webequipe-pdf-indexing-method-pro-link" href="<?php echo esc_url( $upgrade_url ); ?>">
				<?php esc_html_e( 'Upgrade to Pro →', 'webequipe-pdf-search' ); ?>
			</a>
		</div>
		<?php
	}

	/**
	 * Render PDF indexing method card grid (free: native + manual; Pro options as upsell).
	 *
	 * @since 1.2.2
	 * @param array $args Field arguments.
	 * @return void
	 */
	public function render_indexing_method_field( $args = array() ) {
		unset( $args );
		$settings     = Database::get_settings();
		$selected     = ! empty( $settings['enable_indexing'] ) ? 'native' : 'manual';
		$upgrade_url  = Pro_Launch_Banner::get_admin_pricing_url();
		$option_name  = 'webequipe_pdf_search_settings[indexing_method]';

		$native_option = array(
			'title'       => __( 'Native Text Extraction', 'webequipe-pdf-search' ),
			'description' => __( 'Fast indexing for normal text-based PDFs. Best for documents where text can already be selected or copied. PDFs are indexed automatically when uploaded.', 'webequipe-pdf-search' ),
		);

		$manual_option = array(
			'title'       => __( 'Manual Only', 'webequipe-pdf-search' ),
			'description' => __( 'PDFs will not be indexed automatically on upload. Use Manage PDFs to index individual files, or Re-index All PDFs to rebuild the entire library (excluded PDFs are still skipped).', 'webequipe-pdf-search' ),
		);

		$ocr_option = array(
			'title'       => __( 'OCR Text Recognition', 'webequipe-pdf-search' ),
			'description' => __( 'Uses OCR to read scanned PDFs and image-based documents. Better accuracy for scanned files, but slower processing. PDFs are indexed automatically when uploaded.', 'webequipe-pdf-search' ),
		);

		$fallback_option = array(
			'title'       => __( 'Native + OCR Fallback', 'webequipe-pdf-search' ),
			'description' => __( 'Runs OCR first, then falls back to native text extraction when OCR is unavailable. Recommended for most websites. PDFs are indexed automatically when uploaded.', 'webequipe-pdf-search' ),
			'recommended' => true,
		);

		echo '<div class="webequipe-pdf-indexing-method-grid" role="radiogroup" aria-label="' . esc_attr__( 'PDF indexing method', 'webequipe-pdf-search' ) . '">';

		$is_native_selected = ( 'native' === $selected );
		$is_manual_selected = ( 'manual' === $selected );
		?>
		<label class="webequipe-pdf-indexing-method-card">
			<div class="webequipe-pdf-indexing-method-card-head">
				<span class="webequipe-pdf-indexing-method-radio-wrap">
					<input
						type="radio"
						class="webequipe-pdf-indexing-method-input"
						name="<?php echo esc_attr( $option_name ); ?>"
						value="native"
						<?php checked( $is_native_selected ); ?>
					>
					<span class="webequipe-pdf-indexing-method-radio" aria-hidden="true"></span>
				</span>
				<span class="webequipe-pdf-indexing-method-card-title"><?php echo esc_html( $native_option['title'] ); ?></span>
			</div>
			<p class="webequipe-pdf-indexing-method-card-desc"><?php echo esc_html( $native_option['description'] ); ?></p>
		</label>
		<?php
		self::render_indexing_method_pro_card( $ocr_option, $upgrade_url, false );

		self::render_indexing_method_pro_card( $fallback_option, $upgrade_url, true );
		?>
		<label class="webequipe-pdf-indexing-method-card">
			<div class="webequipe-pdf-indexing-method-card-head">
				<span class="webequipe-pdf-indexing-method-radio-wrap">
					<input
						type="radio"
						class="webequipe-pdf-indexing-method-input"
						name="<?php echo esc_attr( $option_name ); ?>"
						value="manual"
						<?php checked( $is_manual_selected ); ?>
					>
					<span class="webequipe-pdf-indexing-method-radio" aria-hidden="true"></span>
				</span>
				<span class="webequipe-pdf-indexing-method-card-title"><?php echo esc_html( $manual_option['title'] ); ?></span>
			</div>
			<p class="webequipe-pdf-indexing-method-card-desc"><?php echo esc_html( $manual_option['description'] ); ?></p>
		</label>
		<?php

		echo '</div>';
	}

	/**
	 * Render one disabled Pro upsell card in the indexing method grid.
	 *
	 * @since 1.2.2
	 * @param array  $option      Card title and description.
	 * @param string $upgrade_url Admin Freemius pricing URL.
	 * @param bool   $recommended Whether to show the Recommended badge.
	 * @return void
	 */
	private static function render_indexing_method_pro_card( $option, $upgrade_url, $recommended ) {
		?>
		<div class="webequipe-pdf-indexing-method-card webequipe-pdf-indexing-method-card--pro">
			<div class="webequipe-pdf-indexing-method-card-head">
				<span class="webequipe-pdf-indexing-method-radio-wrap webequipe-pdf-indexing-method-radio-wrap--disabled">
					<span class="webequipe-pdf-indexing-method-radio webequipe-pdf-indexing-method-radio--disabled" aria-hidden="true"></span>
				</span>
				<span class="webequipe-pdf-indexing-method-card-title">
					<?php echo esc_html( $option['title'] ); ?>
					<?php if ( $recommended ) : ?>
						<span class="webequipe-pdf-indexing-method-badge webequipe-pdf-indexing-method-badge--recommended"><?php esc_html_e( 'Recommended', 'webequipe-pdf-search' ); ?></span>
					<?php endif; ?>
					<span class="webequipe-pdf-indexing-method-badge webequipe-pdf-indexing-method-badge--pro"><?php esc_html_e( 'Available on Pro', 'webequipe-pdf-search' ); ?></span>
				</span>
			</div>
			<p class="webequipe-pdf-indexing-method-card-desc"><?php echo esc_html( $option['description'] ); ?></p>
			<a class="webequipe-pdf-indexing-method-pro-link" href="<?php echo esc_url( $upgrade_url ); ?>">
				<?php esc_html_e( 'Upgrade to Pro →', 'webequipe-pdf-search' ); ?>
			</a>
		</div>
		<?php
	}

	/**
	 * Render checkbox field (control only; row label rendered by card).
	 *
	 * @since 1.0.0
	 * @param array $args Field arguments.
	 * @return void
	 */
	public function render_checkbox_field( $args ) {
		$settings = Database::get_settings();
		$field    = $args['field'];
		$value    = isset( $settings[ $field ] ) ? $settings[ $field ] : false;
		$layout   = isset( $args['layout'] ) ? $args['layout'] : 'checkbox';
		$label    = isset( $args['label'] ) ? $args['label'] : '';

		if ( 'toggle' === $layout ) {
			?>
			<label class="webequipe-pdf-toggle">
				<input type="checkbox" class="webequipe-pdf-toggle-input" name="webequipe_pdf_search_settings[<?php echo esc_attr( $field ); ?>]" value="1" <?php checked( $value, true ); ?>>
				<span class="webequipe-pdf-toggle-track" aria-hidden="true"></span>
				<span class="screen-reader-text"><?php echo esc_html( isset( $args['row_description'] ) ? $args['row_description'] : $field ); ?></span>
			</label>
			<?php
			return;
		}

		?>
		<label class="webequipe-pdf-settings-checkbox-control">
			<input type="checkbox" name="webequipe_pdf_search_settings[<?php echo esc_attr( $field ); ?>]" value="1" <?php checked( $value, true ); ?>>
			<?php
			if ( $label ) {
				echo esc_html( $label );
			}
			?>
		</label>
		<?php
		$this->render_control_hint( $args );
	}

	/**
	 * Render number field (control only).
	 *
	 * @since 1.0.0
	 * @param array $args Field arguments.
	 * @return void
	 */
	public function render_number_field( $args ) {
		$settings = Database::get_settings();
		$field    = $args['field'];
		$value    = isset( $settings[ $field ] ) ? $settings[ $field ] : '';
		$min      = isset( $args['min'] ) ? $args['min'] : '';
		$max      = isset( $args['max'] ) ? $args['max'] : '';
		$step     = isset( $args['step'] ) ? $args['step'] : 1;
		?>
		<input type="number" name="webequipe_pdf_search_settings[<?php echo esc_attr( $field ); ?>]" value="<?php echo esc_attr( $value ); ?>" min="<?php echo esc_attr( $min ); ?>" max="<?php echo esc_attr( $max ); ?>" step="<?php echo esc_attr( $step ); ?>" class="webequipe-pdf-settings-input">
		<?php
		if ( ! empty( $args['show_size_readable'] ) && is_numeric( $value ) && (int) $value > 0 ) {
			printf(
				'<p class="webequipe-pdf-settings-control-hint">%s</p>',
				esc_html(
					sprintf(
						/* translators: %s: human-readable file size. */
						__( 'Equivalent to approximately %s.', 'webequipe-pdf-search' ),
						size_format( (int) $value )
					)
				)
			);
		}
		$this->render_control_hint( $args );
	}

	/**
	 * Render text field (control only).
	 *
	 * @since 1.0.0
	 * @param array $args Field arguments.
	 * @return void
	 */
	public function render_text_field( $args ) {
		$settings = Database::get_settings();
		$field    = $args['field'];
		$value    = isset( $settings[ $field ] ) ? $settings[ $field ] : '';
		?>
		<input type="text" name="webequipe_pdf_search_settings[<?php echo esc_attr( $field ); ?>]" value="<?php echo esc_attr( $value ); ?>" class="webequipe-pdf-settings-input">
		<?php
		if ( ! empty( $args['show_server_memory'] ) ) {
			$server_memory = ini_get( 'memory_limit' );
			if ( $server_memory ) {
				printf(
					'<p class="webequipe-pdf-settings-control-hint">%s</p>',
					esc_html(
						sprintf(
							/* translators: %s: PHP memory_limit from server. */
							__( 'Current server memory limit: %s.', 'webequipe-pdf-search' ),
							$server_memory
						)
					)
				);
			}
		}
		$this->render_control_hint( $args );
	}

	/**
	 * Output optional helper text below a settings control.
	 *
	 * @since 1.2.1
	 * @param array $args Field arguments.
	 * @return void
	 */
	private function render_control_hint( $args ) {
		if ( empty( $args['control_hint'] ) ) {
			return;
		}
		printf(
			'<p class="webequipe-pdf-settings-control-hint">%s</p>',
			esc_html( $args['control_hint'] )
		);
	}

	/**
	 * Enqueue admin assets
	 *
	 * @since 1.0.0
	 * @param string $hook Current admin page hook.
	 * @return void
	 */
	public function enqueue_admin_assets( $hook ) {
		wp_enqueue_style(
			'webequipe-pdf-search-admin-menu-icon',
			WEBEQUIPE_PDF_SEARCH_URL . 'admin/css/menu-icon.css',
			array(),
			WEBEQUIPE_PDF_SEARCH_VERSION
		);

		$is_plugin_page = ( false !== strpos( $hook, 'webequipe-pdf-search' ) )
			|| 'upload.php' === $hook
			|| 'post.php' === $hook;

		if ( ! $is_plugin_page ) {
			return;
		}

		if ( 'post.php' === $hook ) {
			$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
			if ( empty( $screen ) || 'attachment' !== $screen->post_type ) {
				return;
			}
		}

		wp_enqueue_style(
			'webequipe-pdf-search-admin',
			WEBEQUIPE_PDF_SEARCH_URL . 'admin/css/admin-style.css',
			array(),
			WEBEQUIPE_PDF_SEARCH_VERSION
		);

		wp_enqueue_script(
			'webequipe-pdf-search-admin',
			WEBEQUIPE_PDF_SEARCH_URL . 'admin/js/admin-script.js',
			array( 'jquery' ),
			WEBEQUIPE_PDF_SEARCH_VERSION,
			true
		);

		wp_localize_script(
			'webequipe-pdf-search-admin',
			'webequipePdfSearch',
			array(
				'ajaxUrl'              => admin_url( 'admin-ajax.php' ),
				'nonce'                => wp_create_nonce( 'webequipe_pdf_search_nonce' ),
				'dismissProBannerNonce' => wp_create_nonce( 'webequipe_dismiss_pro_banner' ),
				'upgradeUrl'           => Pro_Launch_Banner::get_admin_pricing_url(),
				'strings' => array(
					'confirmReindexAll'    => __( 'Re-index every PDF in your media library now? This runs regardless of Manual Only mode. Excluded PDFs are skipped. Keep this page open until progress finishes.', 'webequipe-pdf-search' ),
					'confirmClear'         => __( 'Are you sure you want to clear the entire index? This cannot be undone.', 'webequipe-pdf-search' ),
					'confirmUnindex'       => __( 'Are you sure you want to remove this PDF from the search index?', 'webequipe-pdf-search' ),
					'confirmExclude'       => __( 'Are you sure you want to exclude this PDF from indexing? It will be skipped in "Re-Index All PDFs".', 'webequipe-pdf-search' ),
					/* translators: %d: number of PDFs. */
					'confirmBulkAction'    => __( 'Are you sure you want to perform this action on %d PDF(s)?', 'webequipe-pdf-search' ),
					'processing'           => __( 'Processing...', 'webequipe-pdf-search' ),
					'statusScheduled'      => __( 'Scheduled', 'webequipe-pdf-search' ),
					'statusScheduledHint'  => __( 'Waiting for next batch', 'webequipe-pdf-search' ),
					'statusStalled'        => __( 'Stalled', 'webequipe-pdf-search' ),
					'statusStalledHint'    => __( 'Background job did not run — use Resume or open the site to trigger WP-Cron', 'webequipe-pdf-search' ),
					'resumeIndexing'       => __( 'Resume', 'webequipe-pdf-search' ),
					'cancelIndexing'       => __( 'Cancel', 'webequipe-pdf-search' ),
					'confirmCancelIndexing' => __( 'Cancel indexing for this PDF? Partial progress will be removed.', 'webequipe-pdf-search' ),
					'indexingCancelled'    => __( 'Indexing cancelled.', 'webequipe-pdf-search' ),
					'completed'            => __( 'Completed', 'webequipe-pdf-search' ),
					'error'                => __( 'An error occurred', 'webequipe-pdf-search' ),
					'errorPrefix'          => __( 'Error:', 'webequipe-pdf-search' ),
					'errorLabel'           => __( 'Error', 'webequipe-pdf-search' ),
					'errorDetailTitle'     => __( 'PDF Indexing Error', 'webequipe-pdf-search' ),
					'close'                => __( 'Close', 'webequipe-pdf-search' ),
					'suggestMaxFileSize'   => __( 'Reduce the PDF size or increase the "Maximum File Size" in the PDF Search settings, then retry indexing.', 'webequipe-pdf-search' ),
					'suggestSecuredPdf'    => __( 'Open the PDF and save or print it to a new, unprotected PDF (no password/security), then upload and index that new file.', 'webequipe-pdf-search' ),
					'suggestSecuredPdfPro' => __( 'Password-protected PDFs must be saved without security before indexing. If the file is a scanned document, PDF Search Pro can use OCR to make an unprotected scanned PDF searchable after you remove protection.', 'webequipe-pdf-search' ),
					'suggestParserMissing' => __( 'Reinstall or update the PDF Search plugin so the required PDF parser library is available on the server.', 'webequipe-pdf-search' ),
					'suggestFileMissing'   => __( 'Re-upload the PDF so the file exists on the server path used by WordPress, then retry indexing.', 'webequipe-pdf-search' ),
					'suggestDatabase'      => __( 'Ensure the plugin\'s custom index table exists and that the database user can write to it. Deactivate and reactivate the plugin to recreate the table if needed.', 'webequipe-pdf-search' ),
					'suggestParsing'       => __( 'Open the PDF in a reader, export or print it to a fresh PDF file, then upload and index that new file.', 'webequipe-pdf-search' ),
					'suggestGeneric'       => __( 'Retry indexing this PDF. If the error persists, try re-saving the file as a new PDF and check your server error logs for more details.', 'webequipe-pdf-search' ),
					'suggestImageOnly'     => __( 'Save or export your document as a text-based PDF from the original source application, then upload and index again.', 'webequipe-pdf-search' ),
					'suggestPartialIndex'  => __( 'Only pages with extractable text are included in search. To index additional content, add it as text in your PDF before uploading.', 'webequipe-pdf-search' ),
					'suggestPartialIndexPro' => __( 'Some pages in this PDF are image-based or scanned, so they are not searchable with the free indexer. PDF Search Pro uses OCR to extract text from scanned pages and image-only content, then includes them in search. Text-based pages you already indexed remain searchable.', 'webequipe-pdf-search' ),
					'suggestImageOnlyPro'  => __( 'This PDF appears to be scanned or image-only. The free plugin cannot extract searchable text from it. PDF Search Pro includes OCR to read scanned PDFs and image-based documents and make them searchable.', 'webequipe-pdf-search' ),
					'suggestImageOnlyFreeTip' => __( 'You can also export a text-based PDF from your original application and index again with the free plugin.', 'webequipe-pdf-search' ),
					'upgradeToProCta'      => __( 'Upgrade to Pro →', 'webequipe-pdf-search' ),
					'warningDetailTitle'   => __( 'Partial PDF Index', 'webequipe-pdf-search' ),
					'partialIndexAriaLabel' => __( 'View partial index details', 'webequipe-pdf-search' ),
					'indexed'              => __( 'Indexed', 'webequipe-pdf-search' ),
					'excluded'             => __( 'Excluded', 'webequipe-pdf-search' ),
					'notIndexed'           => __( 'Not Indexed', 'webequipe-pdf-search' ),
					'index'                => __( 'Index', 'webequipe-pdf-search' ),
					'unindex'              => __( 'Unindex', 'webequipe-pdf-search' ),
					'excludeAction'        => __( 'Exclude', 'webequipe-pdf-search' ),
					'include'              => __( 'Include', 'webequipe-pdf-search' ),
					'retry'                => __( 'Retry', 'webequipe-pdf-search' ),
					'copied'               => __( 'Copied!', 'webequipe-pdf-search' ),
					'noPDFs'               => __( 'No PDFs found', 'webequipe-pdf-search' ),
					'selectAction'         => __( 'Please select an action', 'webequipe-pdf-search' ),
					'selectPDFs'           => __( 'Please select at least one PDF', 'webequipe-pdf-search' ),
					'justNow'              => __( 'Just now', 'webequipe-pdf-search' ),
					'emptyValue'           => __( '—', 'webequipe-pdf-search' ),
					'startingReindex'      => __( 'Starting re-index…', 'webequipe-pdf-search' ),
					'reindexInProgressNotice' => __( 'Re-indexing in progress. Please do not refresh this page or click other indexing buttons until this finishes.', 'webequipe-pdf-search' ),
					/* translators: 1: processed count, 2: failed count, 3: percentage complete. */
					'progressTemplate'     => __( 'Processed: %1$d | Failed: %2$d | %3$d%', 'webequipe-pdf-search' ),
					/* translators: 1: processed count, 2: failed count. */
					'completedTemplate'    => __( 'Completed | Processed: %1$d | Failed: %2$d', 'webequipe-pdf-search' ),
					'noActivity'           => __( 'No index activity found', 'webequipe-pdf-search' ),
					'loadingActivity'      => __( 'Loading activity...', 'webequipe-pdf-search' ),
					'view'                 => __( 'View', 'webequipe-pdf-search' ),
					'mediaId'              => __( 'Media ID', 'webequipe-pdf-search' ),
					/* translators: 1: start record number, 2: end record number, 3: total records. */
					'showingRecords'       => __( 'Showing %1$d–%2$d of %3$d activity records', 'webequipe-pdf-search' ),
					/* translators: 1: current page number, 2: total page count. */
					'pageOf'               => __( 'Page %1$d of %2$d', 'webequipe-pdf-search' ),
					'previous'             => __( 'Previous', 'webequipe-pdf-search' ),
					'next'                 => __( 'Next', 'webequipe-pdf-search' ),
					'statusCompleted'      => __( 'completed', 'webequipe-pdf-search' ),
					'statusProcessing'     => __( 'processing', 'webequipe-pdf-search' ),
					'statusPending'        => __( 'pending', 'webequipe-pdf-search' ),
					'statusFailed'         => __( 'failed', 'webequipe-pdf-search' ),
					'statusCancelled'      => __( 'cancelled', 'webequipe-pdf-search' ),
					/* translators: 1: pages indexed so far, 2: total pages in the PDF. */
					'pagesProgress'        => __( '%1$d of %2$d pages', 'webequipe-pdf-search' ),
					'extractionNative'     => __( 'Native', 'webequipe-pdf-search' ),
					'visibilityPublic'     => __( 'Public', 'webequipe-pdf-search' ),
					/* translators: %s: formatted date/time. */
					'lastScanned'          => __( 'Last scanned: %s', 'webequipe-pdf-search' ),
					'lastScannedPending'   => __( 'Scanning library…', 'webequipe-pdf-search' ),
					/* translators: %s: formatted date/time. */
					'lastUpdated'          => __( 'Last updated: %s', 'webequipe-pdf-search' ),
					/* translators: 1: first record number, 2: last record number, 3: total records. */
					'paginationSummary'    => __( '%1$d–%2$d of %3$d', 'webequipe-pdf-search' ),
				),
				'dateFormat' => get_option( 'date_format' ) . ' ' . get_option( 'time_format' ),
			)
		);

		webequipe_pdf_search_add_script_strategy( 'webequipe-pdf-search-admin', 'admin' );
	}

	/**
	 * AJAX handler for re-indexing
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function ajax_reindex() {
		check_ajax_referer( 'webequipe_pdf_search_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'webequipe-pdf-search' ) ) );
		}

		$offset       = isset( $_POST['offset'] ) ? absint( wp_unslash( $_POST['offset'] ) ) : 0;
		$pending_only = ! empty( $_POST['pending_only'] );

		webequipe_pdf_search_set_indexing_time_limit( 300 );

		$indexer = new PDF_Indexer();
		$results = $indexer->reindex_all( $offset, 1, $pending_only );

		$results['batch_size'] = 1;

		wp_send_json_success( $results );
	}

	/**
	 * Count PDF attachments in the media library.
	 *
	 * @since 1.2.2
	 * @return int
	 */
	private function count_pdf_attachments() {
		return count( Database::get_pdf_attachment_ids() );
	}

	/**
	 * Count PDFs that Re-index All will process (non-excluded).
	 *
	 * @since 1.2.2
	 * @return int
	 */
	private function count_reindexable_pdf_attachments() {
		return count( Database::get_reindexable_pdf_attachment_ids() );
	}

	/**
	 * AJAX handler for re-indexing single PDF
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function ajax_reindex_single() {
		check_ajax_referer( 'webequipe_pdf_search_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'webequipe-pdf-search' ) ) );
		}

		$post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0;

		if ( ! $post_id ) {
			wp_send_json_error( array( 'message' => __( 'Invalid post ID', 'webequipe-pdf-search' ) ) );
		}

		// Clear exclusion flag when manually re-indexing.
		delete_post_meta( $post_id, '_webequipe_pdf_search_excluded' );

		$indexer = new PDF_Indexer();
		$success = $indexer->index_pdf( $post_id );

		if ( $success ) {
			wp_send_json_success( array( 'message' => __( 'PDF re-indexed successfully', 'webequipe-pdf-search' ) ) );
		} else {
			$error = get_post_meta( $post_id, '_webequipe_pdf_index_error', true );
			$error = $error ? sanitize_text_field( $error ) : '';
			wp_send_json_error( array( 'message' => $error ? $error : __( 'Failed to re-index PDF', 'webequipe-pdf-search' ) ) );
		}
	}

	/**
	 * AJAX handler for cancelling an in-flight indexing job.
	 *
	 * @since 1.2.2
	 * @return void
	 */
	public function ajax_cancel_indexing() {
		check_ajax_referer( 'webequipe_pdf_search_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'webequipe-pdf-search' ) ) );
		}

		$post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0;

		if ( ! $post_id ) {
			wp_send_json_error( array( 'message' => __( 'Invalid post ID', 'webequipe-pdf-search' ) ) );
		}

		if ( ! Database::cancel_indexing_job( $post_id ) ) {
			wp_send_json_error( array( 'message' => __( 'This PDF is not currently being indexed.', 'webequipe-pdf-search' ) ) );
		}

		wp_send_json_success( array( 'message' => __( 'Indexing cancelled.', 'webequipe-pdf-search' ) ) );
	}

	/**
	 * AJAX handler: process stalled indexing jobs (no cron queued).
	 *
	 * @since 1.2.2
	 * @return void
	 */
	public function ajax_resume_indexing() {
		check_ajax_referer( 'webequipe_pdf_search_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'webequipe-pdf-search' ) ) );
		}

		$post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0;
		$limit   = isset( $_POST['limit'] ) ? absint( wp_unslash( $_POST['limit'] ) ) : 3;

		$indexer = new PDF_Indexer();
		if ( $post_id ) {
			if ( ! Database::is_indexing_job_stalled( $post_id ) && ! Database::indexing_job_in_flight( $post_id ) ) {
				wp_send_json_error( array( 'message' => __( 'This PDF is not waiting to be indexed.', 'webequipe-pdf-search' ) ) );
			}
			$indexer->process_page_batch( $post_id );
			wp_send_json_success( array( 'resumed' => 1 ) );
		}

		$resumed = $indexer->resume_stalled_jobs( $limit );
		wp_send_json_success( array( 'resumed' => $resumed ) );
	}

	/**
	 * AJAX handler for unindexing single PDF
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function ajax_unindex_single() {
		check_ajax_referer( 'webequipe_pdf_search_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'webequipe-pdf-search' ) ) );
		}

		$post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0;

		if ( ! $post_id ) {
			wp_send_json_error( array( 'message' => __( 'Invalid post ID', 'webequipe-pdf-search' ) ) );
		}

		// Remove from index.
		$success = Database::delete_pdf( $post_id );

		// Also remove any error meta.
		Database::clear_index_error( $post_id );
		Database::clear_index_warning( $post_id );
		delete_post_meta( $post_id, '_webequipe_pdf_indexing_status' );

		// Note: Do NOT set exclusion flag - unindexed files can be re-indexed later.

		if ( $success ) {
			wp_send_json_success( array( 'message' => __( 'PDF removed from search index', 'webequipe-pdf-search' ) ) );
		} else {
			wp_send_json_success( array( 'message' => __( 'PDF was not in the index', 'webequipe-pdf-search' ) ) );
		}
	}

	/**
	 * AJAX handler for excluding single PDF
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function ajax_exclude_single() {
		check_ajax_referer( 'webequipe_pdf_search_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'webequipe-pdf-search' ) ) );
		}

		$post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0;

		if ( ! $post_id ) {
			wp_send_json_error( array( 'message' => __( 'Invalid post ID', 'webequipe-pdf-search' ) ) );
		}

		// Remove from index if indexed.
		Database::delete_pdf( $post_id );

		// Remove any error meta.
		Database::clear_index_error( $post_id );
		Database::clear_index_warning( $post_id );
		delete_post_meta( $post_id, '_webequipe_pdf_indexing_status' );

		// Set excluded flag - this PDF will be skipped in "Re-Index All PDFs".
		update_post_meta( $post_id, '_webequipe_pdf_search_excluded', true );

		wp_send_json_success( array( 'message' => __( 'PDF excluded from search indexing', 'webequipe-pdf-search' ) ) );
	}

	/**
	 * AJAX handler for including single PDF (clear exclusion only; does not index).
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function ajax_include_single() {
		check_ajax_referer( 'webequipe_pdf_search_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'webequipe-pdf-search' ) ) );
		}

		$post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0;

		if ( ! $post_id ) {
			wp_send_json_error( array( 'message' => __( 'Invalid post ID', 'webequipe-pdf-search' ) ) );
		}

		delete_post_meta( $post_id, '_webequipe_pdf_search_excluded' );

		wp_send_json_success( array( 'message' => __( 'PDF included; you can now index it.', 'webequipe-pdf-search' ) ) );
	}

	/**
	 * AJAX handler for clearing index
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function ajax_clear_index() {
		check_ajax_referer( 'webequipe_pdf_search_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'webequipe-pdf-search' ) ) );
		}

		if ( ! Database::clear_entire_index() ) {
			wp_send_json_error( array( 'message' => __( 'Failed to clear the index. Please try again.', 'webequipe-pdf-search' ) ) );
		}

		wp_send_json_success( array( 'message' => __( 'Index cleared successfully', 'webequipe-pdf-search' ) ) );
	}

	/**
	 * AJAX handler for scanning PDFs
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function ajax_scan_pdfs() {
		check_ajax_referer( 'webequipe_pdf_search_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'webequipe-pdf-search' ) ) );
		}

		$offset        = isset( $_POST['offset'] ) ? absint( wp_unslash( $_POST['offset'] ) ) : 0;
		$limit         = isset( $_POST['limit'] ) ? absint( wp_unslash( $_POST['limit'] ) ) : 50;
		$search        = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';
		$status_filter = isset( $_POST['status_filter'] ) ? sanitize_text_field( wp_unslash( $_POST['status_filter'] ) ) : 'all';

		$result = $this->get_pdf_list( $offset, $limit, $search, $status_filter );

		$result['scanned_at'] = current_time( 'mysql' );
		$result['scanned_at_display'] = date_i18n(
			get_option( 'date_format' ) . ' ' . get_option( 'time_format' ),
			strtotime( $result['scanned_at'] )
		);

		wp_send_json_success( $result );
	}

	/**
	 * AJAX handler for index activity list
	 *
	 * @since 1.2.0
	 * @return void
	 */
	public function ajax_activity_list() {
		check_ajax_referer( 'webequipe_pdf_search_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'webequipe-pdf-search' ) ) );
		}

		$offset        = isset( $_POST['offset'] ) ? absint( wp_unslash( $_POST['offset'] ) ) : 0;
		$limit         = isset( $_POST['limit'] ) ? absint( wp_unslash( $_POST['limit'] ) ) : 20;
		$search        = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';
		$status_filter = isset( $_POST['status_filter'] ) ? sanitize_text_field( wp_unslash( $_POST['status_filter'] ) ) : 'all';

		$list  = Database::get_activity_list(
			array(
				'offset'        => $offset,
				'limit'         => $limit,
				'search'        => $search,
				'status_filter' => $status_filter,
			)
		);
		$stats = Database::get_activity_statistics();

		wp_send_json_success(
			array(
				'stats'   => $stats,
				'records' => $list['records'],
				'total'   => $list['total'],
				'offset'  => $list['offset'],
				'limit'   => $list['limit'],
				'updated' => current_time( 'mysql' ),
			)
		);
	}

	/**
	 * AJAX handler for exporting index activity as CSV
	 *
	 * @since 1.2.0
	 * @return void
	 */
	public function ajax_export_activity() {
		check_ajax_referer( 'webequipe_pdf_search_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Insufficient permissions', 'webequipe-pdf-search' ), '', array( 'response' => 403 ) );
		}

		$search        = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';
		$status_filter = isset( $_POST['status_filter'] ) ? sanitize_text_field( wp_unslash( $_POST['status_filter'] ) ) : 'all';

		$list = Database::get_activity_list(
			array(
				'offset'        => 0,
				'limit'         => 10000,
				'search'        => $search,
				'status_filter' => $status_filter,
			)
		);

		$filename = 'index-activity-' . gmdate( 'Y-m-d' ) . '.csv';

		nocache_headers();
		header( 'Content-Type: text/csv; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename="' . $filename . '"' );

		$output = fopen( 'php://output', 'w' );
		if ( false === $output ) {
			wp_die( esc_html__( 'Could not export activity log.', 'webequipe-pdf-search' ) );
		}

		fputcsv(
			$output,
			array(
				__( 'Document', 'webequipe-pdf-search' ),
				__( 'Media ID', 'webequipe-pdf-search' ),
				__( 'Pages', 'webequipe-pdf-search' ),
				__( 'Status', 'webequipe-pdf-search' ),
				__( 'Indexed', 'webequipe-pdf-search' ),
				__( 'Updated', 'webequipe-pdf-search' ),
				__( 'Error', 'webequipe-pdf-search' ),
			)
		);

		foreach ( $list['records'] as $record ) {
			fputcsv(
				$output,
				array(
					$record['title'],
					$record['post_id'],
					$record['page_count'],
					! empty( $record['display_status'] ) ? $record['display_status'] : $record['index_status'],
					$record['started_at'],
					$record['display_date'],
					$record['error_message'],
				)
			);
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- php://output stream for CSV download.
		fclose( $output );
		exit;
	}

	/**
	 * Get PDF list with status
	 *
	 * @since 1.0.0
	 * @param int    $offset Offset for pagination.
	 * @param int    $limit Number of PDFs per page.
	 * @param string $search Search term.
	 * @param string $status_filter Status filter.
	 * @return array PDF list data
	 */
	private function get_pdf_list( $offset = 0, $limit = 50, $search = '', $status_filter = 'all' ) {
		$pdfs  = array();
		$stats = array(
			'total'       => 0,
			'indexed'     => 0,
			'not_indexed' => 0,
			'excluded'    => 0,
			'errors'      => 0,
		);

		$all_attachments = get_posts(
			array(
				'post_type'              => 'attachment',
				'post_mime_type'         => 'application/pdf',
				'post_status'            => 'inherit',
				'posts_per_page'         => -1,
				'orderby'                => 'date',
				'order'                  => 'DESC',
				'no_found_rows'          => true,
				'update_post_meta_cache' => false,
				'update_post_term_cache' => false,
			)
		);

		// Process all PDFs to determine status and calculate stats.
		$filtered_pdfs = array();
		foreach ( $all_attachments as $attachment ) {
			if ( ! empty( $search ) ) {
				$title_match = false !== stripos( (string) $attachment->post_title, $search );
				$slug_match  = false !== stripos( (string) $attachment->post_name, $search );
				if ( ! $title_match && ! $slug_match ) {
					continue;
				}
			}

			$post_id = (int) $attachment->ID;

			// Get status.
			$indexed  = Database::get_indexed_pdf( $post_id );
			$error    = Database::get_index_error_message( $post_id );
			$warning  = Database::get_index_warning_message( $post_id );
			$status   = Database::get_pdf_display_status( $post_id );

			if ( 'error' === $status ) {
				++$stats['errors'];
			} elseif ( 'excluded' === $status ) {
				++$stats['excluded'];
			} elseif ( 'indexed' === $status ) {
				++$stats['indexed'];
			} else {
				++$stats['not_indexed'];
			}

			// Get file info.
			$file_path = get_attached_file( $post_id );
			$file_size = $file_path && file_exists( $file_path ) ? filesize( $file_path ) : 0;
			$title     = ! empty( $attachment->post_title ) ? $attachment->post_title : $attachment->post_name;
			$filename  = basename( $file_path ? $file_path : '' );
			$edit_link = admin_url(
				add_query_arg(
					array(
						'post'   => absint( $post_id ),
						'action' => 'edit',
					),
					'post.php'
				)
			);

			$indexed_file  = Database::get_indexed_file( $post_id );
			$page_count    = $indexed_file ? (int) $indexed_file->page_count : 0;
			$pages_indexed = $indexed_file ? (int) $indexed_file->pages_indexed : 0;

			$pdf_data = array(
				'id'                  => absint( $post_id ),
				'title'               => sanitize_text_field( $title ),
				'filename'            => sanitize_file_name( $filename ),
				'url'                 => esc_url_raw( wp_get_attachment_url( $post_id ) ),
				'edit_link'           => esc_url_raw( $edit_link ),
				'file_size'           => absint( $file_size ),
				'file_size_formatted' => $file_size ? sanitize_text_field( size_format( $file_size ) ) : esc_html__( '—', 'webequipe-pdf-search' ),
				'page_count'          => $page_count,
				'pages_indexed'       => $pages_indexed,
				'status'              => in_array( $status, array( 'indexed', 'not_indexed', 'excluded', 'error', 'processing', 'scheduled', 'stalled' ), true ) ? $status : 'not_indexed',
				'indexing_stalled'    => Database::is_indexing_job_stalled( $post_id ),
				'indexed_date'        => $indexed ? sanitize_text_field( $indexed->indexed_date ) : null,
				'error_message'       => $error ? sanitize_text_field( $error ) : null,
				'index_warning'       => $warning ? sanitize_text_field( $warning ) : null,
			);

			// Apply status filter.
			$matches_filter = 'all' === $status_filter || $status === $status_filter;
			if ( 'processing' === $status_filter && in_array( $status, array( 'processing', 'scheduled', 'stalled' ), true ) ) {
				$matches_filter = true;
			}
			if ( $matches_filter ) {
				$filtered_pdfs[] = $pdf_data;
			}
		}

		$stats['total'] = count( $all_attachments );

		// Apply pagination after filtering.
		$total = count( $filtered_pdfs );
		$pdfs  = array_slice( $filtered_pdfs, $offset, $limit );

		return array(
			'pdfs'   => $pdfs,
			'total'  => $total,
			'offset' => $offset,
			'limit'  => $limit,
			'stats'  => $stats,
		);
	}

	/**
	 * AJAX handler for bulk actions on PDF list
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function ajax_bulk_action() {
		check_ajax_referer( 'webequipe_pdf_search_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'webequipe-pdf-search' ) ) );
		}

		$action   = isset( $_POST['action_type'] ) ? sanitize_text_field( wp_unslash( $_POST['action_type'] ) ) : '';
		$post_ids = isset( $_POST['post_ids'] ) ? array_map( 'absint', wp_unslash( $_POST['post_ids'] ) ) : array();

		if ( empty( $action ) || empty( $post_ids ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid request', 'webequipe-pdf-search' ) ) );
		}

		$processed = 0;
		$failed    = 0;
		$skipped   = 0;
		$indexer   = new PDF_Indexer();

		foreach ( $post_ids as $post_id ) {
			// Verify it's a PDF.
			if ( 'application/pdf' !== get_post_mime_type( $post_id ) ) {
				++$skipped;
				continue;
			}

			// Skip excluded PDFs when bulk indexing - they should remain excluded.
			// Note: Single manual "Index" action will clear exclusion (intentional override).
			if ( 'index' === $action && get_post_meta( $post_id, '_webequipe_pdf_search_excluded', true ) ) {
				++$skipped;
				continue;
			}

			switch ( $action ) {
				case 'index':
					// Index the PDF.
					if ( $indexer->index_pdf( $post_id ) ) {
						++$processed;
					} else {
						++$failed;
					}
					break;

				case 'unindex':
					// Remove from index.
					if ( Database::delete_pdf( $post_id ) ) {
						++$processed;
					} else {
						++$skipped;
					}
					// Clear error meta.
					Database::clear_index_error( $post_id );
		Database::clear_index_warning( $post_id );
					delete_post_meta( $post_id, '_webequipe_pdf_indexing_status' );
					break;

				case 'include':
					// Clear exclusion flag (makes PDF eligible for indexing).
					if ( get_post_meta( $post_id, '_webequipe_pdf_search_excluded', true ) ) {
						delete_post_meta( $post_id, '_webequipe_pdf_search_excluded' );
						++$processed;
					} else {
						++$skipped; // Wasn't excluded.
					}
					break;

				case 'exclude':
					// Remove from index.
					Database::delete_pdf( $post_id );
					// Remove error meta.
					Database::clear_index_error( $post_id );
		Database::clear_index_warning( $post_id );
					delete_post_meta( $post_id, '_webequipe_pdf_indexing_status' );
					// Set exclusion flag.
					update_post_meta( $post_id, '_webequipe_pdf_search_excluded', true );
					++$processed;
					break;
			}
		}

		wp_send_json_success(
			array(
				'message'   => sprintf(
					// translators: %1$d: processed count, %2$d: failed count, %3$d: skipped count.
					__( 'Processed: %1$d, Failed: %2$d, Skipped: %3$d', 'webequipe-pdf-search' ),
					$processed,
					$failed,
					$skipped
				),
				'processed' => $processed,
				'failed'    => $failed,
				'skipped'   => $skipped,
			)
		);
	}

	/**
	 * Add custom column to media library
	 *
	 * @since 1.0.0
	 * @param array $columns Media columns.
	 * @return array Modified columns
	 */
	public function add_media_column( $columns ) {
		$columns['webequipe_pdf_indexed'] = __( 'Search Indexed', 'webequipe-pdf-search' );
		return $columns;
	}

	/**
	 * Display custom column content
	 *
	 * @since 1.0.0
	 * @param string $column_name Column name.
	 * @param int    $post_id Post ID.
	 * @return void
	 */
	public function display_media_column( $column_name, $post_id ) {
		if ( 'webequipe_pdf_indexed' !== $column_name ) {
			return;
		}

		$mime_type = get_post_mime_type( $post_id );
		if ( 'application/pdf' !== $mime_type ) {
			// Show empty cell for non-PDF files so column is visible.
			echo esc_html__( '—', 'webequipe-pdf-search' );
			return;
		}

		$indexed = Database::get_indexed_pdf( $post_id );
		$error   = Database::get_index_error_message( $post_id );
		$warning = Database::get_index_warning_message( $post_id );
		$status  = Database::get_pdf_display_status( $post_id );

		echo '<div class="webequipe-pdf-index-status" data-post-id="' . esc_attr( $post_id ) . '">';

		if ( 'error' === $status && $error ) {
			echo '<span class="status-badge status-error" title="' . esc_attr( $error ) . '"><span class="dashicons dashicons-warning"></span> ' . esc_html__( 'Error', 'webequipe-pdf-search' ) . '</span>';
			echo '<button type="button" class="button-link webequipe-pdf-error-details" data-error="' . esc_attr( $error ) . '" aria-label="' . esc_attr__( 'View error details and suggestions', 'webequipe-pdf-search' ) . '"><span class="dashicons dashicons-info-outline"></span></button>';
			echo '<div class="webequipe-pdf-actions">';
			echo '<button type="button" class="button button-small webequipe-pdf-reindex-single" data-post-id="' . esc_attr( $post_id ) . '">' . esc_html__( 'Retry', 'webequipe-pdf-search' ) . '</button>';
			echo '<span class="spinner"></span>';
			echo '</div>';
		} elseif ( 'excluded' === $status ) {
			echo '<span class="status-badge status-excluded"><span class="dashicons dashicons-hidden"></span> ' . esc_html__( 'Excluded', 'webequipe-pdf-search' ) . '</span>';
			echo '<div class="webequipe-pdf-actions">';
			echo '<button type="button" class="button button-small webequipe-pdf-include-single" data-post-id="' . esc_attr( $post_id ) . '" title="' . esc_attr__( 'Include this PDF so it can be indexed', 'webequipe-pdf-search' ) . '">' . esc_html__( 'Include', 'webequipe-pdf-search' ) . '</button>';
			echo '<span class="spinner"></span>';
			echo '</div>';
		} elseif ( 'indexed' === $status && $indexed ) {
			$indexed_title = $warning ? $warning : '';
			echo '<span class="status-badge status-indexed"' . ( $indexed_title ? ' title="' . esc_attr( $indexed_title ) . '"' : '' ) . '><span class="dashicons dashicons-yes-alt"></span> ' . esc_html__( 'Indexed', 'webequipe-pdf-search' ) . '</span>';
			if ( $warning ) {
				echo '<button type="button" class="button-link webequipe-pdf-index-warning" data-warning="' . esc_attr( $warning ) . '" aria-label="' . esc_attr__( 'View partial index details', 'webequipe-pdf-search' ) . '"><span class="dashicons dashicons-info-outline"></span></button>';
			}
			echo '<br><small>' . esc_html( date_i18n( get_option( 'date_format' ), strtotime( $indexed->indexed_date ) ) ) . '</small>';
			echo '<div class="webequipe-pdf-actions">';
			echo '<button type="button" class="button button-small webequipe-pdf-unindex-single" data-post-id="' . esc_attr( $post_id ) . '" title="' . esc_attr__( 'Remove from search index (can be re-indexed later)', 'webequipe-pdf-search' ) . '">' . esc_html__( 'Unindex', 'webequipe-pdf-search' ) . '</button> ';
			echo '<button type="button" class="button button-small webequipe-pdf-exclude-single" data-post-id="' . esc_attr( $post_id ) . '" title="' . esc_attr__( 'Exclude from indexing permanently', 'webequipe-pdf-search' ) . '">' . esc_html__( 'Exclude', 'webequipe-pdf-search' ) . '</button>';
			echo '<span class="spinner"></span>';
			echo '</div>';
		} elseif ( in_array( $status, array( 'processing', 'scheduled' ), true ) ) {
			$label = 'scheduled' === $status ? __( 'Scheduled', 'webequipe-pdf-search' ) : __( 'Processing...', 'webequipe-pdf-search' );
			$class = 'scheduled' === $status ? 'status-scheduled' : 'status-processing';
			echo '<span class="status-badge ' . esc_attr( $class ) . '"><span class="dashicons dashicons-update"></span> ' . esc_html( $label ) . '</span>';
		} else {
			echo '<span class="status-badge status-not-indexed"><span class="dashicons dashicons-minus"></span> ' . esc_html__( 'Not Indexed', 'webequipe-pdf-search' ) . '</span>';
			echo '<div class="webequipe-pdf-actions">';
			echo '<button type="button" class="button button-small webequipe-pdf-reindex-single" data-post-id="' . esc_attr( $post_id ) . '" title="' . esc_attr__( 'Add to search index', 'webequipe-pdf-search' ) . '">' . esc_html__( 'Index', 'webequipe-pdf-search' ) . '</button> ';
			echo '<button type="button" class="button button-small webequipe-pdf-exclude-single" data-post-id="' . esc_attr( $post_id ) . '" title="' . esc_attr__( 'Exclude from indexing permanently', 'webequipe-pdf-search' ) . '">' . esc_html__( 'Exclude', 'webequipe-pdf-search' ) . '</button>';
			echo '<span class="spinner"></span>';
			echo '</div>';
		}

		echo '</div>';
	}

	/**
	 * Add meta box to attachment edit page
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function add_attachment_meta_box() {
		add_meta_box(
			'webequipe-pdf-search-status',
			__( 'PDF Search Index Status', 'webequipe-pdf-search' ),
			array( $this, 'render_attachment_meta_box' ),
			'attachment',
			'side',
			'high'
		);
	}

	/**
	 * Render attachment meta box
	 *
	 * @since 1.0.0
	 * @param \WP_Post $post Post object.
	 * @return void
	 */
	public function render_attachment_meta_box( $post ) {
		$mime_type = get_post_mime_type( $post->ID );
		if ( 'application/pdf' !== $mime_type ) {
			echo '<p>' . esc_html__( 'This attachment is not a PDF file.', 'webequipe-pdf-search' ) . '</p>';
			return;
		}

		$indexed        = Database::get_indexed_pdf( $post->ID );
		$error          = Database::get_index_error_message( $post->ID );
		$warning        = Database::get_index_warning_message( $post->ID );
		$display_status = Database::get_pdf_display_status( $post->ID );
		$status         = get_post_meta( $post->ID, '_webequipe_pdf_indexing_status', true );
		$excluded       = 'excluded' === $display_status;

		include WEBEQUIPE_PDF_SEARCH_DIR . 'admin/views/attachment-meta-box.php';
	}

	/**
	 * Add bulk actions to Media Library
	 *
	 * @since 1.0.0
	 * @param array $actions Existing bulk actions.
	 * @return array Modified actions
	 */
	public function add_media_bulk_actions( $actions ) {
		$actions['webequipe_pdf_index']   = __( 'Index PDFs', 'webequipe-pdf-search' );
		$actions['webequipe_pdf_unindex'] = __( 'Unindex PDFs', 'webequipe-pdf-search' );
		$actions['webequipe_pdf_include'] = __( 'Include PDFs', 'webequipe-pdf-search' );
		$actions['webequipe_pdf_exclude'] = __( 'Exclude PDFs', 'webequipe-pdf-search' );
		return $actions;
	}

	/**
	 * Handle bulk actions for Media Library
	 *
	 * @since 1.0.0
	 * @param string $redirect_url Redirect URL.
	 * @param string $action Action name.
	 * @param array  $post_ids Selected post IDs.
	 * @return string Modified redirect URL
	 */
	public function handle_media_bulk_actions( $redirect_url, $action, $post_ids ) {
		// Verify nonce for bulk actions. WordPress core verifies nonce before calling this filter,
		// but we verify again for extra security.
		if ( ! isset( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'bulk-media' ) ) {
			return $redirect_url;
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			return $redirect_url;
		}

		// Check if action is one of ours.
		if ( ! in_array( $action, array( 'webequipe_pdf_index', 'webequipe_pdf_unindex', 'webequipe_pdf_include', 'webequipe_pdf_exclude' ), true ) ) {
			return $redirect_url;
		}

		$processed = 0;
		$failed    = 0;
		$skipped   = 0;

		foreach ( $post_ids as $post_id ) {
			$post_id = absint( $post_id );

			// Only process PDFs.
			if ( 'application/pdf' !== get_post_mime_type( $post_id ) ) {
				++$skipped;
				continue;
			}

			// Skip excluded PDFs when bulk indexing - they should remain excluded.
			// Note: Single manual "Index" action will clear exclusion (intentional override).
			if ( 'webequipe_pdf_index' === $action && get_post_meta( $post_id, '_webequipe_pdf_search_excluded', true ) ) {
				++$skipped;
				continue;
			}

			switch ( $action ) {
				case 'webequipe_pdf_index':
					// Index the PDF.
					$indexer = new PDF_Indexer();
					if ( $indexer->index_pdf( $post_id ) ) {
						++$processed;
					} else {
						++$failed;
					}
					break;

				case 'webequipe_pdf_unindex':
					// Remove from index (don't set exclusion flag).
					if ( Database::delete_pdf( $post_id ) ) {
						++$processed;
					} else {
						++$skipped; // Wasn't indexed.
					}
					// Clear error meta.
					Database::clear_index_error( $post_id );
		Database::clear_index_warning( $post_id );
					delete_post_meta( $post_id, '_webequipe_pdf_indexing_status' );
					break;

				case 'webequipe_pdf_include':
					// Clear exclusion flag (makes PDF eligible for indexing).
					if ( get_post_meta( $post_id, '_webequipe_pdf_search_excluded', true ) ) {
						delete_post_meta( $post_id, '_webequipe_pdf_search_excluded' );
						++$processed;
					} else {
						++$skipped; // Wasn't excluded.
					}
					break;

				case 'webequipe_pdf_exclude':
					// Remove from index.
					Database::delete_pdf( $post_id );

					// Remove error meta.
					Database::clear_index_error( $post_id );
		Database::clear_index_warning( $post_id );
					delete_post_meta( $post_id, '_webequipe_pdf_indexing_status' );

					// Set exclusion flag.
					update_post_meta( $post_id, '_webequipe_pdf_search_excluded', true );
					++$processed;
					break;
			}
		}

		// Store results in transient for admin notice (transient pattern: do not read $_GET in admin_notices).
		$message = '';
		$type    = 'success';
		switch ( $action ) {
			case 'webequipe_pdf_index':
				$message = sprintf(
					/* translators: 1: number processed, 2: number failed, 3: number skipped */
					_n(
						'Indexed %1$d PDF. %2$d failed. %3$d skipped (not PDFs).',
						'Indexed %1$d PDFs. %2$d failed. %3$d skipped (not PDFs).',
						$processed,
						'webequipe-pdf-search'
					),
					$processed,
					$failed,
					$skipped
				);
				if ( $failed > 0 ) {
					$type = 'warning';
				}
				break;
			case 'webequipe_pdf_unindex':
				$message = sprintf(
					/* translators: 1: number processed, 2: number skipped */
					_n(
						'Removed %1$d PDF from index. %2$d skipped (not indexed).',
						'Removed %1$d PDFs from index. %2$d skipped (not indexed).',
						$processed,
						'webequipe-pdf-search'
					),
					$processed,
					$skipped
				);
				break;
			case 'webequipe_pdf_include':
				$message = sprintf(
					/* translators: 1: number processed, 2: number skipped */
					_n(
						'Included %1$d PDF. %2$d skipped (not excluded).',
						'Included %1$d PDFs. %2$d skipped (not excluded).',
						$processed,
						'webequipe-pdf-search'
					),
					$processed,
					$skipped
				);
				break;
			case 'webequipe_pdf_exclude':
				$message = sprintf(
					/* translators: 1: number processed, 2: number skipped */
					_n(
						'Excluded %1$d PDF. %2$d skipped (not PDFs).',
						'Excluded %1$d PDFs. %2$d skipped (not PDFs).',
						$processed,
						'webequipe-pdf-search'
					),
					$processed,
					$skipped
				);
				break;
		}

		set_transient(
			'webequipe_pdf_bulk_action_message_' . get_current_user_id(),
			array(
				'message' => $message,
				'type'    => $type,
			),
			30
		);

		$redirect_url = remove_query_arg( array( 'webequipe_pdf_index', 'webequipe_pdf_unindex', 'webequipe_pdf_include', 'webequipe_pdf_exclude', 'action', 'action2', 'ids' ), $redirect_url );

		return $redirect_url;
	}

	/**
	 * Display admin notices for bulk actions.
	 *
	 * Does not read $_GET or $_REQUEST. Notice content is stored in a transient
	 * by handle_media_bulk_actions() after nonce and capability checks.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function bulk_action_admin_notices() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		$transient_key = 'webequipe_pdf_bulk_action_message_' . get_current_user_id();
		$data          = get_transient( $transient_key );
		if ( false === $data || empty( $data['message'] ) ) {
			return;
		}

		$message = $data['message'];
		$type    = isset( $data['type'] ) ? $data['type'] : 'success';
		$class   = 'warning' === $type ? 'notice notice-warning is-dismissible' : 'notice notice-success is-dismissible';

		printf(
			'<div class="%1$s"><p>%2$s</p></div>',
			esc_attr( $class ),
			esc_html( $message )
		);

		delete_transient( $transient_key );
	}

	/**
	 * Add Settings link to plugin action links
	 *
	 * @since 1.0.0
	 * @param array $links Existing plugin action links.
	 * @return array Modified plugin action links
	 */
	public function add_plugin_action_links( $links ) {
		$settings_link = sprintf(
			'<a href="%s">%s</a>',
			esc_url( admin_url( 'admin.php?page=webequipe-pdf-search-settings' ) ),
			esc_html__( 'Settings', 'webequipe-pdf-search' )
		);
		array_unshift( $links, $settings_link );
		return $links;
	}

	/**
	 * Admin notice when legacy index is in use (re-index for per-page search).
	 *
	 * @since 1.2.0
	 * @return void
	 */
	public function maybe_show_legacy_index_notice() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		if ( ! Database::uses_legacy_index() ) {
			return;
		}

		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		if ( ! $screen || strpos( $screen->id, 'webequipe-pdf-search' ) === false ) {
			return;
		}

		echo '<div class="notice notice-info"><p>';
		echo esc_html__( 'Per-page PDF indexing is available. Run Re-index All PDFs to rebuild your index and search the full text of each document.', 'webequipe-pdf-search' );
		echo '</p></div>';
	}
}
