<?php
/**
 * Main plugin controller.
 *
 * @package PufferPDF
 */

defined( 'ABSPATH' ) || exit;

/**
 * Registers the admin converter and secure draft-import endpoint.
 */
final class PufferPDF_Plugin {

	/** Maximum accepted PDF size for the browser converter. */
	private const MAX_PDF_BYTES = 20971520;

	/** Maximum page count accepted in inspection metadata. */
	private const MAX_PAGE_COUNT = 2000;

	/**
	 * Singleton controller instance.
	 *
	 * @var self|null
	 */
	private static $instance = null;

	/**
	 * Converter admin-screen hook.
	 *
	 * @var string
	 */
	private $screen_hook = '';

	/**
	 * Conversion-record service.
	 *
	 * @var PufferPDF_Records
	 */
	private $records;

	/**
	 * Gutenberg block validator.
	 *
	 * @var PufferPDF_Block_Contract
	 */
	private $block_contract;

	/**
	 * Transaction and lock service.
	 *
	 * @var PufferPDF_Storage
	 */
	private $storage;

	/**
	 * Destination capability service.
	 *
	 * @var PufferPDF_Destinations
	 */
	private $destinations;

	/**
	 * Server-owned publication planner.
	 *
	 * @var PufferPDF_Publication_Planner
	 */
	private $publication_planner;

	/**
	 * Publication and undo service.
	 *
	 * @var PufferPDF_Publications
	 */
	private $publications;

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

		return self::$instance;
	}

	/**
	 * Register WordPress hooks.
	 *
	 * @return void
	 */
	public function init() {
		$this->records             = new PufferPDF_Records();
		$this->block_contract      = new PufferPDF_Block_Contract();
		$this->storage             = new PufferPDF_Storage();
		$this->destinations        = new PufferPDF_Destinations();
		$this->publication_planner = new PufferPDF_Publication_Planner(
			$this->block_contract,
			$this->destinations,
			$this->records
		);
		$this->publications        = new PufferPDF_Publications(
			$this->publication_planner,
			$this->block_contract,
			$this->records,
			$this->storage
		);

		add_action( 'init', array( $this->records, 'register_post_type' ) );
		add_action( 'init', array( $this->publications, 'register_post_type' ) );
		add_action( 'admin_menu', array( $this, 'register_admin_page' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
		add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
	}

	/**
	 * Register PufferPDF as a focused utility under Tools.
	 *
	 * @return void
	 */
	public function register_admin_page() {
		$this->screen_hook = add_management_page(
			__( 'PufferPDF', 'pufferpdf' ),
			__( 'PufferPDF', 'pufferpdf' ),
			'edit_posts',
			'pufferpdf',
			array( $this, 'render_admin_page' )
		);
	}

	/**
	 * Load assets only on the converter screen.
	 *
	 * @param string $hook_suffix Current admin screen hook.
	 * @return void
	 */
	public function enqueue_admin_assets( $hook_suffix ) {
		if ( $this->screen_hook !== $hook_suffix ) {
			return;
		}

		$style_path  = PUFFERPDF_PATH . 'assets/css/admin.css';
		$script_path = PUFFERPDF_PATH . 'assets/js/admin.js';

		wp_enqueue_media();

		wp_enqueue_style(
			'pufferpdf-admin',
			PUFFERPDF_URL . 'assets/css/admin.css',
			array(),
			file_exists( $style_path ) ? (string) filemtime( $style_path ) : PUFFERPDF_VERSION
		);

		wp_enqueue_script(
			'pufferpdf-admin',
			PUFFERPDF_URL . 'assets/js/admin.js',
			array( 'wp-blocks', 'wp-block-library' ),
			file_exists( $script_path ) ? (string) filemtime( $script_path ) : PUFFERPDF_VERSION,
			true
		);
	}

	/**
	 * Return the bounded browser configuration rendered with the admin page.
	 *
	 * Keeping this data in HTML avoids executable inline JavaScript and lets a
	 * site apply a nonce-free strict Content Security Policy to the plugin.
	 *
	 * @return array<string, int|string> Browser configuration.
	 */
	private function get_admin_settings() {
		return array(
			'importUrl'       => esc_url_raw( rest_url( 'pufferpdf/v1/import' ) ),
			'attachmentsUrl'  => esc_url_raw( rest_url( 'pufferpdf/v1/attachments' ) ),
			'destinationsUrl' => esc_url_raw( rest_url( 'pufferpdf/v1/destinations' ) ),
			'plansUrl'        => esc_url_raw( rest_url( 'pufferpdf/v1/plans' ) ),
			'publicationsUrl' => esc_url_raw( rest_url( 'pufferpdf/v1/publications' ) ),
			'nonce'           => wp_create_nonce( 'wp_rest' ),
			'workerUrl'       => esc_url_raw( $this->get_worker_url() ),
			'maxBytes'        => self::MAX_PDF_BYTES,
		);
	}

	/**
	 * Build one content-derived URL version for the complete Worker graph.
	 *
	 * The Worker propagates this value to its generated module wrapper and WASM
	 * request, preventing a browser or intermediary cache from mixing revisions.
	 *
	 * @return string Versioned same-origin Worker URL.
	 */
	private function get_worker_url() {
		$asset_paths = array(
			'assets/js/pdf-worker.js',
			'assets/vendor/pdf-inspector/pdf_inspector_wasm.js',
			'assets/vendor/pdf-inspector/pdf_inspector_wasm_bg.wasm',
		);
		$fingerprint = PUFFERPDF_VERSION;

		foreach ( $asset_paths as $relative_path ) {
			$absolute_path = PUFFERPDF_PATH . $relative_path;
			$asset_hash    = is_readable( $absolute_path ) ? hash_file( 'sha256', $absolute_path ) : false;
			$fingerprint  .= '|' . $relative_path . ':' . ( is_string( $asset_hash ) ? $asset_hash : 'missing' );
		}

		$version = substr( hash( 'sha256', $fingerprint ), 0, 20 );

		return add_query_arg(
			'ver',
			$version,
			PUFFERPDF_URL . 'assets/js/pdf-worker.js'
		);
	}

	/**
	 * Render the converter screen.
	 *
	 * @return void
	 */
	public function render_admin_page() {
		if ( ! current_user_can( 'edit_posts' ) ) {
			wp_die( esc_html__( 'You do not have permission to use PufferPDF.', 'pufferpdf' ) );
		}

		$pufferpdf_settings = $this->get_admin_settings();

		require PUFFERPDF_PATH . 'admin/views/converter.php';
	}

	/**
	 * Register source, planning, publication, and draft-import endpoints.
	 *
	 * @return void
	 */
	public function register_rest_routes() {
		register_rest_route(
			'pufferpdf/v1',
			'/attachments',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'list_pdf_attachments' ),
				'permission_callback' => array( $this, 'can_list_pdf_attachments' ),
				'args'                => array(
					'page'         => array(
						'type'    => 'integer',
						'default' => 1,
						'minimum' => 1,
					),
					'perPage'      => array(
						'type'    => 'integer',
						'default' => 20,
						'minimum' => 1,
						'maximum' => 50,
					),
					'search'       => array(
						'type'              => 'string',
						'default'           => '',
						'maxLength'         => 100,
						'sanitize_callback' => 'sanitize_text_field',
					),
					'attachmentId' => array(
						'type'    => 'integer',
						'minimum' => 1,
					),
				),
			)
		);

		register_rest_route(
			'pufferpdf/v1',
			'/import',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'import_draft' ),
				'permission_callback' => array( $this, 'can_import_draft' ),
				'args'                => array(
					'title'                     => array(
						'required'          => true,
						'type'              => 'string',
						'maxLength'         => 200,
						'sanitize_callback' => 'sanitize_text_field',
					),
					'content'                   => array(
						'required' => true,
						'type'     => 'string',
					),
					'sourceFile'                => array(
						'type'              => 'string',
						'maxLength'         => 255,
						'sanitize_callback' => 'sanitize_file_name',
					),
					'engineVersion'             => array(
						'type'              => 'string',
						'maxLength'         => 32,
						'sanitize_callback' => 'sanitize_text_field',
					),
					'pdfType'                   => array(
						'required' => true,
						'type'     => 'string',
						'enum'     => array( 'TextBased', 'Scanned', 'ImageBased', 'Mixed' ),
					),
					'pageCount'                 => array(
						'required' => true,
						'type'     => 'integer',
						'minimum'  => 1,
						'maximum'  => self::MAX_PAGE_COUNT,
					),
					'pagesNeedingOcr'           => array(
						'type'     => 'array',
						'default'  => array(),
						'maxItems' => self::MAX_PAGE_COUNT,
						'items'    => array(
							'type'    => 'integer',
							'minimum' => 1,
						),
					),
					'hasEncodingIssues'         => array(
						'type'    => 'boolean',
						'default' => false,
					),
					'acknowledgeIncomplete'     => array(
						'type'    => 'boolean',
						'default' => false,
					),
					'attachmentId'              => array(
						'type'    => 'integer',
						'minimum' => 1,
					),
					'sourceHash'                => array(
						'type'              => 'string',
						'maxLength'         => 64,
						'sanitize_callback' => 'sanitize_text_field',
					),
					'profile'                   => array(
						'type'    => 'string',
						'enum'    => array( 'fidelity', 'compact' ),
						'default' => 'fidelity',
					),
					'pageDividers'              => array(
						'type'    => 'boolean',
						'default' => true,
					),
					'reprocessRecordId'         => array(
						'type'    => 'integer',
						'minimum' => 1,
					),
					'expectedTargetModifiedGmt' => array(
						'type'              => 'string',
						'maxLength'         => 19,
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
			)
		);

		register_rest_route(
			'pufferpdf/v1',
			'/destinations',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this->destinations, 'get_destinations' ),
				'permission_callback' => array( $this->destinations, 'permissions_check' ),
				'args'                => array(
					'postType' => array(
						'type'              => 'string',
						'default'           => '',
						'maxLength'         => 20,
						'sanitize_callback' => 'sanitize_key',
					),
				),
			)
		);

		register_rest_route(
			'pufferpdf/v1',
			'/plans',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this->publication_planner, 'preview' ),
				'permission_callback' => array( $this->publication_planner, 'permissions_check' ),
			)
		);

		register_rest_route(
			'pufferpdf/v1',
			'/publications',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this->publications, 'create' ),
				'permission_callback' => array( $this->publication_planner, 'permissions_check' ),
			)
		);

		register_rest_route(
			'pufferpdf/v1',
			'/publications/(?P<id>\d+)/undo',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this->publications, 'undo' ),
				'permission_callback' => array( $this->publication_planner, 'permissions_check' ),
				'args'                => array(
					'id' => array(
						'type'    => 'integer',
						'minimum' => 1,
					),
				),
			)
		);
	}

	/**
	 * Check whether the current user may create posts.
	 *
	 * @return bool
	 */
	public function can_import_draft() {
		$post_type = get_post_type_object( 'post' );

		return $post_type && current_user_can( $post_type->cap->create_posts );
	}

	/**
	 * Check whether the current user may inspect Media Library PDFs.
	 *
	 * @return bool
	 */
	public function can_list_pdf_attachments() {
		return is_user_logged_in() && current_user_can( 'upload_files' );
	}

	/**
	 * Return a bounded, hash-aware inventory of Media Library PDFs.
	 *
	 * @param WP_REST_Request $request REST request.
	 * @return WP_REST_Response
	 */
	public function list_pdf_attachments( WP_REST_Request $request ) {
		return $this->records->get_attachment_inventory( $request, self::MAX_PDF_BYTES );
	}

	/**
	 * Create a draft post from reviewed serialized core blocks.
	 *
	 * @param WP_REST_Request $request REST request.
	 * @return WP_REST_Response|WP_Error
	 */
	public function import_draft( WP_REST_Request $request ) {
		$title                 = trim( (string) $request->get_param( 'title' ) );
		$content               = (string) $request->get_param( 'content' );
		$pdf_type              = (string) $request->get_param( 'pdfType' );
		$page_count            = (int) $request->get_param( 'pageCount' );
		$pages_needing_ocr     = $this->normalize_page_numbers(
			(array) $request->get_param( 'pagesNeedingOcr' ),
			$page_count
		);
		$has_encoding_issues   = (bool) $request->get_param( 'hasEncodingIssues' );
		$acknowledged          = (bool) $request->get_param( 'acknowledgeIncomplete' );
		$attachment_id         = absint( $request->get_param( 'attachmentId' ) );
		$reprocess_record_id   = absint( $request->get_param( 'reprocessRecordId' ) );
		$requested_source_hash = strtolower( trim( (string) $request->get_param( 'sourceHash' ) ) );
		$engine_version        = sanitize_text_field( (string) $request->get_param( 'engineVersion' ) );

		if ( '' === $title ) {
			return new WP_Error(
				'pufferpdf_missing_title',
				__( 'Add a title before creating the draft.', 'pufferpdf' ),
				array( 'status' => 400 )
			);
		}

		if ( '' !== $requested_source_hash && ! preg_match( '/\A[a-f0-9]{64}\z/', $requested_source_hash ) ) {
			return new WP_Error(
				'pufferpdf_invalid_source_hash',
				__( 'The source hash must be a complete SHA-256 value.', 'pufferpdf' ),
				array( 'status' => 400 )
			);
		}

		if ( 'Scanned' === $pdf_type || 'ImageBased' === $pdf_type ) {
			return new WP_Error(
				'pufferpdf_ocr_required',
				__( 'This PDF needs OCR before it can be imported.', 'pufferpdf' ),
				array( 'status' => 422 )
			);
		}

		if ( ( 'Mixed' === $pdf_type || ! empty( $pages_needing_ocr ) || $has_encoding_issues ) && ! $acknowledged ) {
			return new WP_Error(
				'pufferpdf_incomplete_not_acknowledged',
				__( 'Confirm that you understand the imported draft may be incomplete.', 'pufferpdf' ),
				array( 'status' => 422 )
			);
		}

		$validated_content = $this->block_contract->validate_content( $content );
		if ( is_wp_error( $validated_content ) ) {
			return $validated_content;
		}
		$sanitized_content = $validated_content['content'];

		if ( $reprocess_record_id && ! $attachment_id ) {
			return new WP_Error(
				'pufferpdf_reprocess_requires_attachment',
				__( 'Reprocessing requires a Media Library PDF.', 'pufferpdf' ),
				array( 'status' => 400 )
			);
		}

		if ( $attachment_id && '' === trim( $engine_version ) ) {
			return new WP_Error(
				'pufferpdf_missing_engine_version',
				__( 'The PDF parser version is required for Media Library conversions.', 'pufferpdf' ),
				array( 'status' => 400 )
			);
		}

		$source_file = sanitize_file_name( (string) $request->get_param( 'sourceFile' ) );
		$target_meta = $this->get_target_metadata(
			$source_file,
			$engine_version,
			$pdf_type,
			$page_count,
			$pages_needing_ocr,
			$has_encoding_issues,
			$attachment_id,
			$requested_source_hash,
			(string) $request->get_param( 'profile' ),
			(bool) $request->get_param( 'pageDividers' )
		);

		if ( ! $attachment_id ) {
			$post_id = $this->insert_draft( $title, $sanitized_content, $target_meta );
			if ( is_wp_error( $post_id ) ) {
				return $post_id;
			}

			return $this->get_import_response( $post_id, null, null, false, false );
		}

		$lock_name = $this->records->acquire_attachment_lock( $attachment_id );
		if ( is_wp_error( $lock_name ) ) {
			return $lock_name;
		}

		$transaction_started = false;
		$touched_post_ids    = array();

		try {
			$source = $this->records->validate_attachment_source(
				$attachment_id,
				$requested_source_hash,
				self::MAX_PDF_BYTES
			);
			if ( is_wp_error( $source ) ) {
				return $source;
			}

			$target_meta['_pufferpdf_source_file']   = $source['filename'];
			$target_meta['_pufferpdf_source_hash']   = $source['hash'];
			$target_meta['_pufferpdf_attachment_id'] = $attachment_id;

			$conversion = array(
				'attachmentId'      => $attachment_id,
				'sourceHash'        => $source['hash'],
				'verification'      => $source['verification'],
				'sourceBytes'       => $source['bytes'],
				'sourceMtime'       => $source['mtime'],
				'engineVersion'     => $engine_version,
				'profile'           => (string) $request->get_param( 'profile' ),
				'pageDividers'      => (bool) $request->get_param( 'pageDividers' ),
				'pdfType'           => $pdf_type,
				'pageCount'         => $page_count,
				'pagesNeedingOcr'   => $pages_needing_ocr,
				'hasEncodingIssues' => $has_encoding_issues,
				'content'           => $sanitized_content,
			);

			$transaction = $this->records->begin_write_transaction();
			if ( is_wp_error( $transaction ) ) {
				return $transaction;
			}
			$transaction_started = true;

			$result = $this->import_attachment_conversion(
				$request,
				$title,
				$target_meta,
				$conversion,
				$reprocess_record_id,
				$touched_post_ids
			);

			if ( is_wp_error( $result ) ) {
				$this->records->rollback_write_transaction( $touched_post_ids );
				$transaction_started = false;

				return $result;
			}

			$source_recheck = $this->records->validate_attachment_source(
				$attachment_id,
				$requested_source_hash,
				self::MAX_PDF_BYTES
			);
			if ( is_wp_error( $source_recheck ) ) {
				$this->records->rollback_write_transaction( $touched_post_ids );
				$transaction_started = false;

				return $source_recheck;
			}

			if (
				$source_recheck['hash'] !== $conversion['sourceHash'] ||
				$source_recheck['verification'] !== $conversion['verification'] ||
				$source_recheck['bytes'] !== $conversion['sourceBytes'] ||
				$source_recheck['mtime'] !== $conversion['sourceMtime']
			) {
				$this->records->rollback_write_transaction( $touched_post_ids );
				$transaction_started = false;

				return new WP_Error(
					'pufferpdf_source_changed',
					__( 'The selected PDF changed while its conversion was being saved. Reload it and try again.', 'pufferpdf' ),
					array( 'status' => 409 )
				);
			}

			if ( ! $this->records->commit_write_transaction() ) {
				$this->records->rollback_write_transaction( $touched_post_ids );
				$transaction_started = false;

				return new WP_Error(
					'pufferpdf_storage_unavailable',
					__( 'PufferPDF could not commit the draft and conversion record safely.', 'pufferpdf' ),
					array( 'status' => 500 )
				);
			}

			$transaction_started = false;

			return $result;
		} catch ( Throwable $exception ) {
			if ( $transaction_started ) {
				$this->records->rollback_write_transaction( $touched_post_ids );
				$transaction_started = false;
			}

			$error_data = array( 'status' => 500 );
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				$error_data['exceptionType'] = get_class( $exception );
			}

			return new WP_Error(
				'pufferpdf_storage_unavailable',
				__( 'PufferPDF could not save this conversion safely.', 'pufferpdf' ),
				$error_data
			);
		} finally {
			$this->records->release_attachment_lock( $lock_name );
		}
	}

	/**
	 * Insert or explicitly reprocess one verified Media Library conversion.
	 *
	 * @param WP_REST_Request $request REST request.
	 * @param string          $title New-draft title.
	 * @param array           $target_meta Target post metadata.
	 * @param array           $conversion Normalized conversion descriptor.
	 * @param int             $reprocess_record_id Explicit record ID or zero.
	 * @param array           $touched_post_ids Post IDs to clear if the transaction rolls back.
	 * @return WP_REST_Response|WP_Error
	 */
	private function import_attachment_conversion(
		WP_REST_Request $request,
		$title,
		array $target_meta,
		array $conversion,
		$reprocess_record_id,
		array &$touched_post_ids
	) {
		$attachment_id   = $conversion['attachmentId'];
		$latest_id       = $this->records->get_latest_record_id( $attachment_id );
		$latest_record   = $latest_id ? $this->records->get_record_data( $latest_id ) : null;
		$selected_record = null;

		if ( $reprocess_record_id ) {
			$selected_record = $this->records->get_record_data( $reprocess_record_id );
			if ( is_wp_error( $selected_record ) ) {
				return $selected_record;
			}

			if ( $selected_record['attachmentId'] !== $attachment_id ) {
				return new WP_Error(
					'pufferpdf_record_attachment_mismatch',
					__( 'The conversion record does not belong to the selected PDF.', 'pufferpdf' ),
					array( 'status' => 409 )
				);
			}

			if ( is_array( $latest_record ) && $selected_record['id'] !== $latest_record['id'] ) {
				$replay_target = null;
				if (
					$latest_record['previousRecordId'] === $selected_record['id'] &&
					$latest_record['targetPostId'] === $selected_record['targetPostId']
				) {
					$replay_target = $this->records->lock_target_post( $latest_record['targetPostId'] );
				}

				if (
					$replay_target &&
					'post' === $replay_target->post_type &&
					'draft' === $replay_target->post_status &&
					current_user_can( 'edit_post', $replay_target->ID ) &&
					$this->records->conversion_is_unchanged( $latest_record, $conversion, $replay_target )
				) {
					return $this->get_import_response( $replay_target->ID, $latest_record['id'], $attachment_id, true, true );
				}

				return new WP_Error(
					'pufferpdf_stale_reprocess_record',
					__( 'A newer conversion record exists for this PDF. Refresh before reprocessing.', 'pufferpdf' ),
					array( 'status' => 409 )
				);
			}

			if ( ! is_array( $latest_record ) ) {
				return new WP_Error(
					'pufferpdf_stale_reprocess_record',
					__( 'The conversion history changed. Refresh before reprocessing.', 'pufferpdf' ),
					array( 'status' => 409 )
				);
			}

			$target = $this->records->lock_target_post( $selected_record['targetPostId'] );
			if ( ! $target || 'post' !== $target->post_type || 'draft' !== $target->post_status ) {
				return new WP_Error(
					'pufferpdf_target_unavailable',
					__( 'The recorded target is no longer an editable draft.', 'pufferpdf' ),
					array( 'status' => 409 )
				);
			}
			$touched_post_ids[] = $target->ID;

			if ( ! current_user_can( 'edit_post', $target->ID ) ) {
				return new WP_Error(
					'pufferpdf_target_forbidden',
					__( 'You do not have permission to update the recorded draft.', 'pufferpdf' ),
					array( 'status' => 403 )
				);
			}

			$expected_modified_gmt = trim( (string) $request->get_param( 'expectedTargetModifiedGmt' ) );
			if ( ! preg_match( '/\A\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\z/', $expected_modified_gmt ) ) {
				return new WP_Error(
					'pufferpdf_missing_target_version',
					__( 'Refresh the Media Library inventory before reprocessing this draft.', 'pufferpdf' ),
					array( 'status' => 400 )
				);
			}

			if ( $expected_modified_gmt !== $this->records->get_target_modified_gmt( $target ) ) {
				return new WP_Error(
					'pufferpdf_target_changed',
					__( 'The target draft changed after you selected it. Reload it before reprocessing.', 'pufferpdf' ),
					array( 'status' => 409 )
				);
			}

			if ( ! $this->records->record_matches_target_content( $selected_record, $target ) ) {
				return new WP_Error(
					'pufferpdf_target_changed',
					__( 'The target draft was edited after its last conversion. Its changes were not overwritten.', 'pufferpdf' ),
					array( 'status' => 409 )
				);
			}

			if ( $this->records->conversion_is_unchanged( $selected_record, $conversion, $target ) ) {
				return $this->get_import_response( $target->ID, $selected_record['id'], $attachment_id, true, true );
			}

			$content_changed = (string) $target->post_content !== $conversion['content'];
			if ( $content_changed && ! wp_revisions_enabled( $target ) ) {
				return new WP_Error(
					'pufferpdf_revisions_required',
					__( 'WordPress revisions must be enabled before PufferPDF can replace draft content safely.', 'pufferpdf' ),
					array( 'status' => 409 )
				);
			}

			$preserved_title = (string) $target->post_title;
			if ( $content_changed ) {
				$prior_content     = (string) $target->post_content;
				$prior_revision_id = $this->ensure_content_revision( $target->ID, $prior_content );
				if ( is_wp_error( $prior_revision_id ) ) {
					return $prior_revision_id;
				}
				$touched_post_ids[] = $prior_revision_id;

				$updated_id       = wp_update_post(
					wp_slash(
						array(
							'ID'           => $target->ID,
							'post_content' => $conversion['content'],
							'meta_input'   => $target_meta,
						)
					),
					true
				);
				$touched_post_ids = array_merge(
					$touched_post_ids,
					array_map( 'absint', array_keys( wp_get_post_revisions( $target->ID ) ) )
				);
				if ( is_wp_error( $updated_id ) ) {
					return $updated_id;
				}

				$preserved_revision_id = $this->find_content_revision( $target->ID, $prior_content );
				if ( ! $preserved_revision_id ) {
					return new WP_Error(
						'pufferpdf_revision_preservation_failed',
						__( 'WordPress could not retain the previous draft content in revision history.', 'pufferpdf' ),
						array( 'status' => 409 )
					);
				}
			} else {
				foreach ( $target_meta as $meta_key => $meta_value ) {
					update_post_meta( $target->ID, $meta_key, $meta_value );
				}
			}

			$stored_target = $this->verify_written_target( $target->ID, $conversion['content'], $preserved_title );
			if ( is_wp_error( $stored_target ) ) {
				return $stored_target;
			}

			$record_id = $this->records->create_record( $conversion, $target->ID, $selected_record['id'] );
			if ( is_wp_error( $record_id ) ) {
				return $record_id;
			}
			$touched_post_ids[] = $record_id;

			$stored_record = $this->records->get_record_data( $record_id );
			$stored_target = $this->verify_written_target( $target->ID, $conversion['content'], $preserved_title );
			if (
				is_wp_error( $stored_record ) ||
				is_wp_error( $stored_target ) ||
				! $this->records->record_matches_conversion( $stored_record, $conversion, $stored_target, $selected_record['id'] )
			) {
				return new WP_Error(
					'pufferpdf_record_write_changed',
					__( 'Another component changed the draft while its conversion record was being saved.', 'pufferpdf' ),
					array( 'status' => 500 )
				);
			}

			return $this->get_import_response( $target->ID, $record_id, $attachment_id, true, false );
		}

		if ( is_array( $latest_record ) ) {
			$target = $this->records->lock_target_post( $latest_record['targetPostId'] );
			if (
				$target &&
				'post' === $target->post_type &&
				current_user_can( 'edit_post', $target->ID ) &&
				$this->records->conversion_is_unchanged( $latest_record, $conversion, $target )
			) {
				return $this->get_import_response( $target->ID, $latest_record['id'], $attachment_id, false, true );
			}

			return new WP_Error(
				'pufferpdf_reprocess_required',
				__( 'This PDF already has a conversion record. Confirm reprocessing before updating its draft.', 'pufferpdf' ),
				array( 'status' => 409 )
			);
		}

		$post_id = $this->insert_draft( $title, $conversion['content'], $target_meta );
		if ( is_wp_error( $post_id ) ) {
			return $post_id;
		}
		$touched_post_ids[] = $post_id;

		$stored_target = $this->verify_written_target( $post_id, $conversion['content'], null );
		if ( is_wp_error( $stored_target ) ) {
			return $stored_target;
		}

		$record_id = $this->records->create_record( $conversion, $post_id, 0 );
		if ( is_wp_error( $record_id ) ) {
			return $record_id;
		}
		$touched_post_ids[] = $record_id;

		$stored_record = $this->records->get_record_data( $record_id );
		$stored_target = $this->verify_written_target( $post_id, $conversion['content'], null );
		if (
			is_wp_error( $stored_record ) ||
			is_wp_error( $stored_target ) ||
			! $this->records->record_matches_conversion( $stored_record, $conversion, $stored_target, 0 )
		) {
			return new WP_Error(
				'pufferpdf_record_write_changed',
				__( 'Another component changed the draft while its conversion record was being saved.', 'pufferpdf' ),
				array( 'status' => 500 )
			);
		}

		return $this->get_import_response( $post_id, $record_id, $attachment_id, false, false );
	}

	/**
	 * Reload and verify a draft after WordPress save hooks have run.
	 *
	 * @param int         $post_id Target post ID.
	 * @param string      $expected_content Reviewed serialized content.
	 * @param string|null $expected_title Existing title that must be preserved, or null for a new draft.
	 * @return WP_Post|WP_Error
	 */
	private function verify_written_target( $post_id, $expected_content, $expected_title ) {
		$target = $this->records->lock_target_post( $post_id );
		if (
			! $target ||
			'post' !== $target->post_type ||
			'draft' !== $target->post_status ||
			(string) $target->post_content !== $expected_content ||
			( null !== $expected_title && (string) $target->post_title !== $expected_title )
		) {
			return new WP_Error(
				'pufferpdf_target_write_changed',
				__( 'Another component changed the draft while PufferPDF was saving it.', 'pufferpdf' ),
				array( 'status' => 500 )
			);
		}

		return $target;
	}

	/**
	 * Ensure the current draft content exists as a non-autosave revision.
	 *
	 * @param int    $post_id Target post ID.
	 * @param string $content Content that must remain recoverable.
	 * @return int|WP_Error
	 */
	private function ensure_content_revision( $post_id, $content ) {
		$revision_id = $this->find_content_revision( $post_id, $content );
		if ( $revision_id ) {
			return $revision_id;
		}

		$revision_id = wp_save_post_revision( $post_id );
		if ( is_wp_error( $revision_id ) || ! $revision_id ) {
			return new WP_Error(
				'pufferpdf_revision_preservation_failed',
				__( 'WordPress could not preserve the current draft before reprocessing.', 'pufferpdf' ),
				array( 'status' => 409 )
			);
		}

		$revision = get_post( $revision_id );
		if ( ! $revision || wp_is_post_autosave( $revision_id ) || (string) $revision->post_content !== $content ) {
			return new WP_Error(
				'pufferpdf_revision_preservation_failed',
				__( 'The saved revision did not preserve the current draft content.', 'pufferpdf' ),
				array( 'status' => 409 )
			);
		}

		return (int) $revision_id;
	}

	/**
	 * Find a non-autosave revision containing an exact serialized draft version.
	 *
	 * @param int    $post_id Target post ID.
	 * @param string $content Serialized content to find.
	 * @return int
	 */
	private function find_content_revision( $post_id, $content ) {
		$revisions = wp_get_post_revisions( $post_id );

		foreach ( $revisions as $revision_id => $revision ) {
			if ( ! wp_is_post_autosave( $revision_id ) && (string) $revision->post_content === $content ) {
				return (int) $revision_id;
			}
		}

		return 0;
	}

	/**
	 * Build the metadata mirrored onto the editable draft target.
	 *
	 * @param string $source_file Source filename.
	 * @param string $engine_version Parser version.
	 * @param string $pdf_type Inspection type.
	 * @param int    $page_count Page count.
	 * @param array  $pages_needing_ocr OCR page numbers.
	 * @param bool   $has_encoding_issues Whether encoding issues were found.
	 * @param int    $attachment_id Attachment ID or zero.
	 * @param string $source_hash Source SHA-256, when available.
	 * @param string $profile Conversion profile.
	 * @param bool   $page_dividers Whether page dividers were requested.
	 * @return array
	 */
	private function get_target_metadata(
		$source_file,
		$engine_version,
		$pdf_type,
		$page_count,
		array $pages_needing_ocr,
		$has_encoding_issues,
		$attachment_id,
		$source_hash,
		$profile,
		$page_dividers
	) {
		return array(
			'_pufferpdf_source_file'         => sanitize_file_name( $source_file ),
			'_pufferpdf_engine_version'      => sanitize_text_field( $engine_version ),
			'_pufferpdf_pdf_type'            => $pdf_type,
			'_pufferpdf_page_count'          => $page_count,
			'_pufferpdf_pages_needing_ocr'   => $pages_needing_ocr,
			'_pufferpdf_has_encoding_issues' => $has_encoding_issues ? 1 : 0,
			'_pufferpdf_attachment_id'       => absint( $attachment_id ),
			'_pufferpdf_source_hash'         => sanitize_text_field( $source_hash ),
			'_pufferpdf_profile'             => 'compact' === $profile ? 'compact' : 'fidelity',
			'_pufferpdf_page_dividers'       => $page_dividers ? 1 : 0,
		);
	}

	/**
	 * Insert a new Gutenberg draft.
	 *
	 * @param string $title Draft title.
	 * @param string $content Validated Gutenberg content.
	 * @param array  $meta Draft metadata.
	 * @return int|WP_Error
	 */
	private function insert_draft( $title, $content, array $meta ) {
		return wp_insert_post(
			wp_slash(
				array(
					'post_type'    => 'post',
					'post_status'  => 'draft',
					'post_title'   => $title,
					'post_content' => $content,
					'meta_input'   => $meta,
				)
			),
			true
		);
	}

	/**
	 * Build the stable import response used by new, repeated, and reprocessed imports.
	 *
	 * @param int      $post_id Target draft ID.
	 * @param int|null $record_id Conversion record ID, when applicable.
	 * @param int|null $attachment_id Attachment ID, when applicable.
	 * @param bool     $reprocessed Whether the request addressed an existing record.
	 * @param bool     $unchanged Whether the request was handled without a write.
	 * @return WP_REST_Response
	 */
	private function get_import_response( $post_id, $record_id, $attachment_id, $reprocessed, $unchanged ) {
		return rest_ensure_response(
			array(
				'id'           => (int) $post_id,
				'title'        => get_the_title( $post_id ),
				'status'       => get_post_status( $post_id ),
				'editUrl'      => get_edit_post_link( $post_id, 'raw' ),
				'recordId'     => null === $record_id ? null : (int) $record_id,
				'attachmentId' => null === $attachment_id ? null : (int) $attachment_id,
				'reprocessed'  => (bool) $reprocessed,
				'unchanged'    => (bool) $unchanged,
			)
		);
	}

	/**
	 * Normalize one-indexed OCR page numbers to the document range.
	 *
	 * @param array $pages      Candidate page numbers.
	 * @param int   $page_count Total number of pages.
	 * @return int[]
	 */
	private function normalize_page_numbers( array $pages, $page_count ) {
		$pages = array_map( 'absint', $pages );
		$pages = array_filter(
			$pages,
			static function ( $page ) use ( $page_count ) {
				return $page >= 1 && $page <= $page_count;
			}
		);
		$pages = array_values( array_unique( $pages ) );
		sort( $pages, SORT_NUMERIC );

		return $pages;
	}
}
