<?php
/**
 * PDF Indexer class for extracting and storing PDF content
 *
 * @package WebEquipe\PDFSearch
 * @since 1.0.0
 */

namespace WebEquipe\PDFSearch;

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

use Smalot\PdfParser\Parser;

/**
 * PDF Indexer class
 */
class PDF_Indexer {

	/**
	 * Post meta key: next page to index (1-based).
	 */
	const META_INDEX_CURSOR = '_webequipe_pdf_index_cursor';

	/**
	 * Post meta key: total pages for current job.
	 */
	const META_INDEX_PAGE_COUNT = '_webequipe_pdf_index_page_count';

	/**
	 * Whether core indexing hooks (cron batches, delete cleanup) are registered.
	 *
	 * @var bool
	 */
	private static $core_hooks_registered = false;

	/**
	 * Whether automatic upload/update indexing hooks are registered.
	 *
	 * @var bool
	 */
	private static $auto_hooks_registered = false;

	/**
	 * When true, page batches run synchronously (Re-index All) without scheduling WP-Cron between batches.
	 *
	 * @var bool
	 */
	private $sync_page_indexing = false;

	/**
	 * Initialize hooks
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		if ( ! class_exists( 'WebEquipe\PDFSearch\Database' ) ) {
			return;
		}

		self::register_core_hooks( $this );

		$settings = Database::get_settings();
		if ( ! empty( $settings['enable_indexing'] ) ) {
			self::register_auto_index_hooks( $this );
		}
	}

	/**
	 * Register hooks required for manual indexing, bulk re-index, and background page batches.
	 *
	 * @since 1.2.2
	 * @param PDF_Indexer $indexer Indexer instance.
	 * @return void
	 */
	private static function register_core_hooks( PDF_Indexer $indexer ) {
		if ( self::$core_hooks_registered ) {
			return;
		}

		self::$core_hooks_registered = true;

		add_action( 'delete_attachment', array( $indexer, 'remove_on_delete' ) );
		add_action( 'webequipe_pdf_search_background_index', array( $indexer, 'background_index' ) );
		add_action( 'webequipe_pdf_search_index_pages', array( $indexer, 'process_page_batch' ) );
	}

	/**
	 * Register automatic indexing on upload/update (Native mode only).
	 *
	 * @since 1.2.2
	 * @param PDF_Indexer $indexer Indexer instance.
	 * @return void
	 */
	private static function register_auto_index_hooks( PDF_Indexer $indexer ) {
		if ( self::$auto_hooks_registered ) {
			return;
		}

		self::$auto_hooks_registered = true;

		add_action( 'add_attachment', array( $indexer, 'index_on_upload' ) );
		add_action( 'edit_attachment', array( $indexer, 'index_on_update' ) );
	}

	/**
	 * Index PDF when uploaded
	 *
	 * @since 1.0.0
	 * @param int $attachment_id Attachment post ID.
	 * @return void
	 */
	public function index_on_upload( $attachment_id ) {
		if ( ! $this->is_pdf( $attachment_id ) ) {
			return;
		}

		if ( get_post_meta( $attachment_id, '_webequipe_pdf_search_excluded', true ) ) {
			return;
		}

		$settings  = Database::get_settings();
		$file_path = get_attached_file( $attachment_id );

		if ( $file_path && file_exists( $file_path ) ) {
			$file_size = filesize( $file_path );
			if ( $file_size > $settings['max_file_size'] ) {
				Database::set_index_error( $attachment_id, __( 'File size exceeds maximum allowed size.', 'webequipe-pdf-search' ) );
				return;
			}

		}

		$this->index_pdf( $attachment_id );
	}

	/**
	 * Re-index PDF when updated
	 *
	 * @since 1.0.0
	 * @param int $attachment_id Attachment post ID.
	 * @return void
	 */
	public function index_on_update( $attachment_id ) {
		if ( ! $this->is_pdf( $attachment_id ) ) {
			return;
		}

		if ( get_post_meta( $attachment_id, '_webequipe_pdf_search_excluded', true ) ) {
			return;
		}

		$this->index_pdf( $attachment_id );
	}

	/**
	 * Remove PDF from index when deleted
	 *
	 * @since 1.0.0
	 * @param int $attachment_id Attachment post ID.
	 * @return void
	 */
	public function remove_on_delete( $attachment_id ) {
		if ( ! $this->is_pdf( $attachment_id ) ) {
			return;
		}

		wp_clear_scheduled_hook( 'webequipe_pdf_search_index_pages', array( $attachment_id ) );

		Database::delete_pdf( $attachment_id );

		Database::clear_index_error( $attachment_id );
		Database::clear_index_warning( $attachment_id );
		delete_post_meta( $attachment_id, '_webequipe_pdf_indexing_status' );
		delete_post_meta( $attachment_id, '_webequipe_pdf_extracted_text' );
		delete_post_meta( $attachment_id, '_webequipe_pdf_search_excluded' );
		delete_post_meta( $attachment_id, self::META_INDEX_CURSOR );
		delete_post_meta( $attachment_id, self::META_INDEX_PAGE_COUNT );
	}

	/**
	 * Background indexing handler (starts per-page job).
	 *
	 * @since 1.0.0
	 * @param int $attachment_id Attachment post ID.
	 * @return void
	 */
	public function background_index( $attachment_id ) {
		update_post_meta( $attachment_id, '_webequipe_pdf_indexing_status', 'processing' );
		$this->start_page_index_job( $attachment_id );
		$this->process_page_batch( $attachment_id );
	}

	/**
	 * Check if attachment is a PDF
	 *
	 * @since 1.0.0
	 * @param int $attachment_id Attachment post ID.
	 * @return bool True if PDF, false otherwise
	 */
	public function is_pdf( $attachment_id ) {
		$mime_type = get_post_mime_type( $attachment_id );
		return 'application/pdf' === $mime_type;
	}

	/**
	 * Index a PDF file (per-page storage).
	 *
	 * @since 1.0.0
	 * @param int   $attachment_id Attachment post ID.
	 * @param array $args          Optional. bulk_reindex (bool) — finish all pages in this request (Re-index All).
	 * @return bool True on success, false on failure
	 */
	public function index_pdf( $attachment_id, $args = array() ) {
		$args         = is_array( $args ) ? $args : array();
		$bulk_reindex = ! empty( $args['bulk_reindex'] );
		if ( ! $this->is_pdf( $attachment_id ) ) {
			return false;
		}

		$previous_sync      = $this->sync_page_indexing;
		$this->sync_page_indexing = $bulk_reindex;

		if ( ! class_exists( 'Smalot\PdfParser\Parser' ) ) {
			$error = __( 'PDF parser library not found. Please reinstall the plugin to restore required files.', 'webequipe-pdf-search' );
			$this->fail_index( $attachment_id, $error );
			$this->sync_page_indexing = $previous_sync;
			return false;
		}

		$settings = Database::get_settings();
		$per_pdf  = isset( $settings['processing_timeout'] ) ? (int) $settings['processing_timeout'] : 60;
		$per_pdf  = max( 10, min( 300, $per_pdf ) );
		webequipe_pdf_search_set_indexing_time_limit( $per_pdf + 30 );

		do_action( 'webequipe_pdf_search_before_index', $attachment_id );

		$file_path = get_attached_file( $attachment_id );
		if ( ! $file_path || ! file_exists( $file_path ) ) {
			$this->fail_index( $attachment_id, __( 'PDF file not found on server.', 'webequipe-pdf-search' ) );
			$this->sync_page_indexing = $previous_sync;
			return false;
		}

		$filesize = filesize( $file_path );
		if ( $bulk_reindex && $this->should_queue_bulk_pdf_for_background( $filesize, $settings ) ) {
			$queued = $this->queue_pdf_for_background_indexing( $attachment_id, $file_path );
			$this->sync_page_indexing = $previous_sync;
			return $queued;
		}

		$raw    = file_get_contents( $file_path, false, null, 0, 4 );
		$header = ( false !== $raw && strlen( $raw ) >= 4 ) ? substr( $raw, 0, 4 ) : false;
		if ( false === $header || '%PDF' !== $header ) {
			$this->fail_index( $attachment_id, __( 'Invalid PDF file format or file is not readable.', 'webequipe-pdf-search' ) );
			$this->sync_page_indexing = $previous_sync;
			return false;
		}

		wp_clear_scheduled_hook( 'webequipe_pdf_search_index_pages', array( $attachment_id ) );

		Database::delete_legacy_pdf( $attachment_id );
		Database::delete_pages_for_post( $attachment_id );

		if ( ! $this->start_page_index_job( $attachment_id ) ) {
			$this->sync_page_indexing = $previous_sync;
			return false;
		}

		$page_count = (int) get_post_meta( $attachment_id, self::META_INDEX_PAGE_COUNT, true );
		$threshold  = isset( $settings['page_index_threshold'] ) ? (int) $settings['page_index_threshold'] : 50;
		$batch      = isset( $settings['pages_per_batch'] ) ? (int) $settings['pages_per_batch'] : 5;
		$batch      = max( 1, min( 50, $batch ) );

		// During Re-index All, large page counts must not block the queue — schedule after the first batch.
		if ( $bulk_reindex && $page_count > $threshold ) {
			$this->process_page_batch( $attachment_id );
			$this->sync_page_indexing = false;
			$this->schedule_page_indexing( $attachment_id );
			$this->sync_page_indexing = $previous_sync;
			return true;
		}

		// Bulk re-index completes each PDF in-process so Manual mode and missing cron traffic cannot leave jobs half-done.
		$use_background = ! $bulk_reindex && ! empty( $settings['enable_background'] ) && $page_count > $threshold;

		if ( $use_background ) {
			$this->process_page_batch( $attachment_id );
			$this->sync_page_indexing = $previous_sync;
			return true;
		}

		$max_loops = (int) ceil( $page_count / $batch ) + 2;
		for ( $i = 0; $i < $max_loops; $i++ ) {
			$status = get_post_meta( $attachment_id, '_webequipe_pdf_indexing_status', true );
			if ( 'completed' === $status ) {
				$this->sync_page_indexing = $previous_sync;
				return true;
			}
			if ( 'failed' === $status ) {
				$this->sync_page_indexing = $previous_sync;
				return false;
			}

			$this->process_page_batch( $attachment_id );
		}

		$status = get_post_meta( $attachment_id, '_webequipe_pdf_indexing_status', true );
		if ( 'completed' === $status ) {
			$this->sync_page_indexing = $previous_sync;
			return true;
		}

		if ( 'failed' === $status ) {
			$this->sync_page_indexing = $previous_sync;
			return false;
		}

		// Could not finish every page in this request — queue the rest so the UI shows Scheduled, not Not indexed.
		if ( Database::indexing_job_in_flight( $attachment_id ) ) {
			$this->schedule_page_indexing( $attachment_id );
			$this->sync_page_indexing = $previous_sync;
			return true;
		}

		$this->sync_page_indexing = $previous_sync;
		return false;
	}

	/**
	 * Initialize file row and page count for a per-page index job.
	 *
	 * @since 1.2.0
	 * @param int $attachment_id Attachment post ID.
	 * @return bool
	 */
	public function start_page_index_job( $attachment_id ) {
		$file_path = get_attached_file( $attachment_id );
		if ( ! $file_path || ! file_exists( $file_path ) ) {
			$this->fail_index( $attachment_id, __( 'PDF file not found on server.', 'webequipe-pdf-search' ) );
			return false;
		}

		try {
			$parser     = new Parser();
			$pdf        = $parser->parseFile( $file_path );
			$details    = $pdf->getDetails();
			$title      = isset( $details['Title'] ) ? $details['Title'] : get_the_title( $attachment_id );
			$pages      = $pdf->getPages();
			$page_count = count( $pages );
		} catch ( \Exception $e ) {
			$this->fail_index(
				$attachment_id,
				sprintf(
					/* translators: %s: exception message */
					__( 'PDF parsing error: %s', 'webequipe-pdf-search' ),
					$e->getMessage()
				)
			);
			return false;
		}

		$file_url = wp_get_attachment_url( $attachment_id );
		$filename = basename( $file_path );
		$filesize = filesize( $file_path );

		Database::upsert_file(
			array(
				'post_id'       => $attachment_id,
				'pdf_title'     => $title,
				'pdf_url'       => $file_url,
				'pdf_filename'  => $filename,
				'pdf_filesize'  => $filesize,
				'page_count'    => $page_count,
				'pages_indexed' => 0,
				'index_status'  => 'processing',
				'error_message' => '',
			)
		);

		Database::log_activity_start(
			$attachment_id,
			array(
				'pdf_title'    => $title,
				'pdf_filename' => $filename,
				'pdf_filesize' => $filesize,
				'page_count'   => $page_count,
			)
		);

		update_post_meta( $attachment_id, self::META_INDEX_CURSOR, 1 );
		update_post_meta( $attachment_id, self::META_INDEX_PAGE_COUNT, $page_count );
		update_post_meta( $attachment_id, '_webequipe_pdf_indexing_status', 'processing' );
		Database::clear_index_error( $attachment_id );
		Database::clear_index_warning( $attachment_id );

		return true;
	}

	/**
	 * Whether a PDF should be queued for background indexing during Re-index All (avoid blocking on huge files).
	 *
	 * @since 1.2.2
	 * @param int   $filesize File size in bytes.
	 * @param array $settings Plugin settings.
	 * @return bool
	 */
	private function should_queue_bulk_pdf_for_background( $filesize, $settings ) {
		$filesize = absint( $filesize );
		// 5 MB — heavy PDFs are parsed on a background request, not in the bulk re-index loop.
		$queue_at = 5 * 1024 * 1024;

		/**
		 * Filter the file-size threshold (bytes) above which Re-index All queues a PDF for background indexing.
		 *
		 * @since 1.2.2
		 * @param int $queue_at_bytes Default 5242880 (5 MB).
		 */
		$queue_at = (int) apply_filters( 'webequipe_pdf_search_bulk_queue_filesize', $queue_at );

		if ( $filesize > $queue_at ) {
			return true;
		}

		$max_file = isset( $settings['max_file_size'] ) ? absint( $settings['max_file_size'] ) : 0;
		if ( $max_file > 0 && $filesize > (int) ( $max_file * 0.8 ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Queue a PDF for background page indexing without parsing the full file during Re-index All.
	 *
	 * @since 1.2.2
	 * @param int    $attachment_id Attachment post ID.
	 * @param string $file_path     Absolute path to the PDF.
	 * @return bool
	 */
	public function queue_pdf_for_background_indexing( $attachment_id, $file_path = '' ) {
		$attachment_id = absint( $attachment_id );
		if ( ! $attachment_id ) {
			return false;
		}

		if ( '' === $file_path ) {
			$file_path = get_attached_file( $attachment_id );
		}

		if ( ! $file_path || ! file_exists( $file_path ) ) {
			$this->fail_index( $attachment_id, __( 'PDF file not found on server.', 'webequipe-pdf-search' ) );
			return false;
		}

		wp_clear_scheduled_hook( 'webequipe_pdf_search_index_pages', array( $attachment_id ) );

		Database::delete_legacy_pdf( $attachment_id );
		Database::delete_pages_for_post( $attachment_id );
		delete_post_meta( $attachment_id, self::META_INDEX_CURSOR );
		delete_post_meta( $attachment_id, self::META_INDEX_PAGE_COUNT );
		Database::clear_index_error( $attachment_id );
		Database::clear_index_warning( $attachment_id );

		$title    = get_the_title( $attachment_id );
		$filename = basename( $file_path );
		$filesize = filesize( $file_path );

		Database::upsert_file(
			array(
				'post_id'       => $attachment_id,
				'pdf_title'     => $title,
				'pdf_url'       => wp_get_attachment_url( $attachment_id ),
				'pdf_filename'  => $filename,
				'pdf_filesize'  => $filesize,
				'page_count'    => 0,
				'pages_indexed' => 0,
				'index_status'  => 'pending',
				'error_message' => '',
			)
		);

		Database::log_activity_start(
			$attachment_id,
			array(
				'pdf_title'    => $title,
				'pdf_filename' => $filename,
				'pdf_filesize' => $filesize,
				'page_count'   => 0,
			)
		);

		$this->schedule_page_indexing( $attachment_id );

		return true;
	}

	/**
	 * Schedule WP-Cron to continue page indexing.
	 *
	 * @since 1.2.0
	 * @param int $attachment_id Attachment post ID.
	 * @return void
	 */
	public function schedule_page_indexing( $attachment_id ) {
		if ( ! wp_next_scheduled( 'webequipe_pdf_search_index_pages', array( $attachment_id ) ) ) {
			wp_schedule_single_event( time() + 1, 'webequipe_pdf_search_index_pages', array( $attachment_id ) );
		}
		update_post_meta( $attachment_id, '_webequipe_pdf_indexing_status', 'scheduled' );

		if ( ! ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) ) {
			spawn_cron();
		}
	}

	/**
	 * Run the next page batch for stalled jobs (no cron event queued).
	 *
	 * @since 1.2.2
	 * @param int $limit Maximum number of attachments to resume.
	 * @return int Number of attachments processed.
	 */
	public function resume_stalled_jobs( $limit = 3 ) {
		$limit   = max( 1, min( 10, (int) $limit ) );
		$post_ids = Database::get_stalled_indexing_post_ids( $limit );
		$resumed  = 0;

		foreach ( $post_ids as $post_id ) {
			$this->process_page_batch( $post_id );
			++$resumed;
		}

		return $resumed;
	}

	/**
	 * Process the next batch of pages for an attachment.
	 *
	 * @since 1.2.0
	 * @param int $attachment_id Attachment post ID.
	 * @return void
	 */
	public function process_page_batch( $attachment_id ) {
		if ( ! $this->is_pdf( $attachment_id ) ) {
			return;
		}

		$settings = Database::get_settings();
		$per_pdf  = isset( $settings['processing_timeout'] ) ? (int) $settings['processing_timeout'] : 60;
		$per_pdf  = max( 10, min( 300, $per_pdf ) );
		webequipe_pdf_search_set_indexing_time_limit( $per_pdf + 30 );

		$cursor     = (int) get_post_meta( $attachment_id, self::META_INDEX_CURSOR, true );
		$page_count = (int) get_post_meta( $attachment_id, self::META_INDEX_PAGE_COUNT, true );
		$batch      = isset( $settings['pages_per_batch'] ) ? (int) $settings['pages_per_batch'] : 5;
		$batch      = max( 1, min( 50, $batch ) );

		if ( $cursor < 1 || $page_count < 1 ) {
			if ( ! $this->start_page_index_job( $attachment_id ) ) {
				return;
			}
			$cursor     = (int) get_post_meta( $attachment_id, self::META_INDEX_CURSOR, true );
			$page_count = (int) get_post_meta( $attachment_id, self::META_INDEX_PAGE_COUNT, true );
		}

		$file_path = get_attached_file( $attachment_id );
		if ( ! $file_path || ! file_exists( $file_path ) ) {
			$this->fail_index( $attachment_id, __( 'PDF file not found on server.', 'webequipe-pdf-search' ) );
			return;
		}

		update_post_meta( $attachment_id, '_webequipe_pdf_indexing_status', 'processing' );

		try {
			$parser = new Parser();
			$pdf    = $parser->parseFile( $file_path );
			$pages  = $pdf->getPages();

			$end_page = min( $page_count, $cursor + $batch - 1 );
			$indexed  = 0;

			for ( $page_num = $cursor; $page_num <= $end_page; $page_num++ ) {
				$page_index = $page_num - 1;
				if ( ! isset( $pages[ $page_index ] ) ) {
					continue;
				}

				$text = $pages[ $page_index ]->getText();
				$text = apply_filters( 'webequipe_pdf_search_extracted_content', $text, $attachment_id, $page_num );

				Database::upsert_page( $attachment_id, $page_num, $text );
				++$indexed;
			}

			$pages_indexed = min( $end_page, $page_count );

			Database::upsert_file(
				array(
					'post_id'       => $attachment_id,
					'pages_indexed' => $pages_indexed,
					'index_status'  => 'processing',
					'last_modified' => current_time( 'mysql' ),
				)
			);

			Database::log_activity_progress( $attachment_id, $pages_indexed );

			$next_cursor = $end_page + 1;
			update_post_meta( $attachment_id, self::META_INDEX_CURSOR, $next_cursor );

			if ( $next_cursor > $page_count ) {
				$this->complete_page_index( $attachment_id, $pdf );
				return;
			}

			if ( ! empty( $settings['enable_background'] ) && ! $this->sync_page_indexing ) {
				$this->schedule_page_indexing( $attachment_id );
			}
		} catch ( \Exception $e ) {
			$this->fail_index(
				$attachment_id,
				sprintf(
					/* translators: %s: exception message */
					__( 'PDF parsing error: %s', 'webequipe-pdf-search' ),
					$e->getMessage()
				)
			);
		}
	}

	/**
	 * Mark indexing complete and store compressed backup meta.
	 *
	 * @since 1.2.0
	 * @param int    $attachment_id Attachment post ID.
	 * @param object $pdf           Parsed PDF object.
	 * @return void
	 */
	private function complete_page_index( $attachment_id, $pdf ) {
		$file       = Database::get_indexed_file( $attachment_id );
		$text       = Database::get_concatenated_page_content( $attachment_id, 50000 );
		$page_count = $file ? (int) $file->page_count : 0;
		if ( ! Database::has_searchable_content( $attachment_id ) ) {
			Database::delete_pages_for_post( $attachment_id );
			$this->fail_index( $attachment_id, Database::get_image_only_error_message_source() );
			return;
		}

		$page_stats = Database::get_page_text_stats( $attachment_id );
		if ( $page_stats['pages_without_text'] > 0 ) {
			Database::set_index_warning(
				$attachment_id,
				sprintf(
					/* translators: 1: number of pages without text, 2: total page count */
					__( 'Indexed with partial content: %1$d of %2$d pages could not be parsed. Image-based pages are not searchable. Search covers text-based pages only.', 'webequipe-pdf-search' ),
					$page_stats['pages_without_text'],
					$page_stats['total_pages']
				)
			);
		} else {
			Database::clear_index_warning( $attachment_id );
		}

		Database::log_activity_complete( $attachment_id, $page_count );

		Database::upsert_file(
			array(
				'post_id'       => $attachment_id,
				'pages_indexed' => $file ? (int) $file->page_count : 0,
				'index_status'  => 'completed',
				'error_message' => '',
				'last_modified' => current_time( 'mysql' ),
			)
		);

		update_post_meta( $attachment_id, '_webequipe_pdf_extracted_text', gzcompress( $text, 6 ) );
		update_post_meta( $attachment_id, '_webequipe_pdf_indexing_status', 'completed' );
		delete_post_meta( $attachment_id, self::META_INDEX_CURSOR );
		delete_post_meta( $attachment_id, self::META_INDEX_PAGE_COUNT );

		Database::clear_index_error( $attachment_id );
		do_action( 'webequipe_pdf_search_after_index', $attachment_id, true );

		$filename = $file ? $file->pdf_filename : '';
		webequipe_pdf_search_log( "Successfully indexed PDF #{$attachment_id} (per-page): {$filename}", 'INFO' );
	}

	/**
	 * Record indexing failure.
	 *
	 * @since 1.2.0
	 * @param int    $attachment_id Attachment post ID.
	 * @param string $error         Error message.
	 * @return void
	 */
	private function fail_index( $attachment_id, $error ) {
		$error = sanitize_text_field( $error );
		Database::clear_index_warning( $attachment_id );
		Database::set_index_error( $attachment_id, $error );
		Database::log_activity_fail( $attachment_id, $error );
		Database::upsert_file(
			array(
				'post_id'       => $attachment_id,
				'index_status'  => 'failed',
				'error_message' => $error,
				'last_modified' => current_time( 'mysql' ),
			)
		);
		update_post_meta( $attachment_id, '_webequipe_pdf_indexing_status', 'failed' );
		webequipe_pdf_search_log( "Failed to index PDF #{$attachment_id}: {$error}", 'ERROR' );
		do_action( 'webequipe_pdf_search_index_failed', $attachment_id, $error );
	}

	/**
	 * Re-index all PDFs
	 *
	 * @since 1.0.0
	 * @param int  $offset       Offset for batch processing.
	 * @param int  $limit        Number of PDFs to process (Re-index All uses 1).
	 * @param bool $pending_only When true, only PDFs not yet fully indexed.
	 * @return array Results array with success count and errors
	 */
	public function reindex_all( $offset = 0, $limit = 10, $pending_only = false ) {
		$results = array(
			'success'          => 0,
			'failed'           => 0,
			'queued'           => 0,
			'errors'           => array(),
			'batch_count'      => 0,
			'total_pdfs'       => 0,
			'has_more'         => false,
			'current_post_id'  => 0,
			'remaining_not_indexed' => 0,
		);

		$offset = max( 0, absint( $offset ) );
		// One PDF per admin request avoids timeouts that skip the rest of a multi-file batch.
		$limit = 1;

		$all_ids = Database::get_reindexable_pdf_attachment_ids( $pending_only );
		$total   = count( $all_ids );
		$slice   = array_slice( $all_ids, $offset, $limit );

		$results['batch_count']           = count( $slice );
		$results['total_pdfs']            = $total;
		$results['has_more'] = ( $offset + count( $slice ) ) < $total;

		foreach ( $slice as $attachment_id ) {
			$results['current_post_id'] = $attachment_id;

			$success = $this->index_pdf(
				$attachment_id,
				array(
					'bulk_reindex' => true,
				)
			);
			if ( $success ) {
				++$results['success'];
				$status = get_post_meta( $attachment_id, '_webequipe_pdf_indexing_status', true );
				if ( in_array( $status, array( 'scheduled', 'processing' ), true ) ) {
					++$results['queued'];
				}
			} else {
				++$results['failed'];
				$error = get_post_meta( $attachment_id, '_webequipe_pdf_index_error', true );
				if ( $error ) {
					$results['errors'][ $attachment_id ] = $error;
				}
			}
		}

		$results['remaining_not_indexed'] = Database::count_reindex_pending_pdfs();

		return $results;
	}
}
