<?php
/**
 * PDF Search integration class
 *
 * @package WebEquipe\PDFSearch
 * @since 1.0.0
 */

namespace WebEquipe\PDFSearch;

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

/**
 * PDF Search class for integrating PDFs into WordPress search
 */
class PDF_Search {

	/**
	 * Tracks which post IDs have already had their PDF content rendered
	 * during the current request to prevent duplicate output when themes
	 * call both the_content() and the_excerpt().
	 *
	 * @var array<int,true>
	 */
	private static $rendered_ids = array();

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

		add_action( 'pre_get_posts', array( $this, 'modify_search_query' ), 10 );
		add_filter( 'posts_results', array( $this, 'merge_pdf_results' ), 10, 2 );
		add_filter( 'the_posts', array( $this, 'mark_pdf_results' ), 10, 2 );
		add_filter( 'the_title', array( $this, 'modify_pdf_title' ), 10, 2 );
		add_filter( 'get_the_excerpt', array( $this, 'modify_pdf_excerpt' ), 10, 2 );
		add_filter( 'wp_trim_excerpt', array( $this, 'modify_pdf_trim_excerpt' ), 10, 2 );
		add_filter( 'the_excerpt', array( $this, 'modify_pdf_rendered_excerpt' ), 10, 1 );
		// Post Excerpt block (and others) use wp_trim_words() which strips HTML; preserve our output.
		add_filter( 'wp_trim_words', array( $this, 'preserve_pdf_excerpt_html' ), 10, 4 );
		add_filter( 'the_content', array( $this, 'modify_pdf_content' ), 10 );
		add_filter( 'post_link', array( $this, 'modify_pdf_permalink' ), 10, 2 );
		add_filter( 'attachment_link', array( $this, 'modify_pdf_permalink' ), 10, 2 );

		// Elementor archive template compatibility.
		add_filter( 'elementor/frontend/the_content', array( $this, 'modify_pdf_content' ), 10 );

		// Universal safety net for block themes: if a search template has no Post Excerpt and no Post
		// Content block, none of the above hooks fire. render_block lets us append our output after any
		// core post block (title, date, etc.) once per post. This is NOT theme-specific — it targets
		// WordPress core blocks that all block themes use.
		add_filter( 'render_block', array( $this, 'maybe_append_pdf_content_to_block' ), 10, 2 );

		// When "Display PDF thumbnail preview" is unchecked, hide the post thumbnail / image for
		// PDFs in search so themes (e.g. Avada post card) do not show an image. Multiple layers
		// because themes vary in how they retrieve and render thumbnails.
		add_filter( 'get_post_metadata', array( $this, 'hide_pdf_thumbnail_meta_when_preview_disabled' ), 10, 4 );
		add_filter( 'post_thumbnail_html', array( $this, 'hide_pdf_thumbnail_html_when_preview_disabled' ), 10, 5 );
		add_filter( 'wp_get_attachment_image', array( $this, 'hide_pdf_attachment_image_when_preview_disabled' ), 10, 5 );
	}

	/**
	 * Check if this search query should include PDF results.
	 *
	 * When "Include PDFs in WordPress search results" is checked (enable_search), indexed PDFs
	 * are merged into the main site search (?s=) so they appear in global search results.
	 *
	 * @since 1.0.0
	 * @param \WP_Query $query WordPress query object.
	 * @return bool True when PDF search should run
	 */
	private function should_handle_query( $query ) {
		if ( ! $query->is_main_query() || ! $query->is_search() || is_admin() ) {
			return false;
		}

		$settings = Database::get_settings();
		// "Include PDFs in WordPress search results" setting: when checked, show PDFs in global search.
		if ( ! empty( $settings['enable_search'] ) ) {
			return true;
		}

		// Allow PDF-only searches from shortcode even if global search is disabled.
		if ( $query->get( 'webequipe_pdf_search' ) ) {
			return true;
		}

		$post_type = $query->get( 'post_type' );
		$mime_type = $query->get( 'post_mime_type' );

		$post_type_match = ( 'attachment' === $post_type ) || ( is_array( $post_type ) && in_array( 'attachment', $post_type, true ) );
		$mime_type_match = ( 'application/pdf' === $mime_type ) || ( is_array( $mime_type ) && in_array( 'application/pdf', $mime_type, true ) );

		return $post_type_match && $mime_type_match;
	}

	/**
	 * Modify search query to prepare for PDF results
	 *
	 * @since 1.0.0
	 * @param \WP_Query $query WordPress query object.
	 * @return void
	 */
	public function modify_search_query( $query ) {
		if ( ! $this->should_handle_query( $query ) ) {
			return;
		}

		// Respect posts_per_page passed via shortcode/query string.
		$posts_per_page = filter_input( INPUT_GET, 'posts_per_page', FILTER_VALIDATE_INT );
		if ( false !== $posts_per_page && null !== $posts_per_page && $posts_per_page > 0 ) {
			$query->set( 'posts_per_page', $posts_per_page );
		}

		// Fire action before search.
		do_action( 'webequipe_pdf_search_before_search', $query );
	}

	/**
	 * Merge PDF results with regular search results
	 *
	 * @since 1.0.0
	 * @param array     $posts Array of post objects.
	 * @param \WP_Query $query WordPress query object.
	 * @return array Modified posts array
	 */
	public function merge_pdf_results( $posts, $query ) {
		if ( ! $this->should_handle_query( $query ) ) {
			return $posts;
		}

		$search_term = get_search_query();
		if ( empty( $search_term ) || strlen( $search_term ) < 2 ) {
			return $posts;
		}

		// Get PDF results.
		$pdf_results = $this->search_pdfs( $search_term, $query );

		if ( empty( $pdf_results ) ) {
			return $posts;
		}

		// WordPress core's prepend_attachment filter prepends the attachment
		// thumbnail for attachment posts.  Since PDF results are attachments,
		// this creates a duplicate preview image next to our own.  Remove it
		// for the search results page; non-attachment posts are unaffected
		// because prepend_attachment already returns early for them.
		remove_filter( 'the_content', 'prepend_attachment' );

		// Merge results.
		$merged = array_merge( $posts, $pdf_results );

		// Remove duplicates.
		$seen   = array();
		$unique = array();
		foreach ( $merged as $post ) {
			$key = $post->ID;
			if ( ! isset( $seen[ $key ] ) ) {
				$seen[ $key ] = true;
				$unique[]     = $post;
			}
		}

		// Sort by relevance score if available.
		usort(
			$unique,
			function ( $a, $b ) {
				$score_a = isset( $a->relevance_score ) ? $a->relevance_score : 0;
				$score_b = isset( $b->relevance_score ) ? $b->relevance_score : 0;
				return $score_b <=> $score_a;
			}
		);

		$total_results = count( $unique );

		// Apply pagination.
		$posts_per_page = absint( $query->get( 'posts_per_page' ) );
		if ( $posts_per_page > 0 ) {
			$paged  = absint( $query->get( 'paged' ) );
			$paged  = $paged > 0 ? $paged : 1;
			$offset = ( $paged - 1 ) * $posts_per_page;
			$unique = array_slice( $unique, $offset, $posts_per_page );
			// Ensure WordPress pagination works with merged results.
			$query->found_posts   = $total_results;
			$query->max_num_pages = (int) ceil( $total_results / $posts_per_page );
		} else {
			$query->found_posts   = $total_results;
			$query->max_num_pages = 1;
		}

		// Fire action after search.
		do_action( 'webequipe_pdf_search_after_search', $unique, $query );

		return $unique;
	}

	/**
	 * Mark PDF results with special properties
	 *
	 * @since 1.0.0
	 * @param array     $posts Array of post objects.
	 * @param \WP_Query $query WordPress query object.
	 * @return array Modified posts array
	 */
	public function mark_pdf_results( $posts, $query ) {
		if ( ! $this->should_handle_query( $query ) ) {
			return $posts;
		}

		$settings     = Database::get_settings();
		$hide_preview = empty( $settings['show_preview'] ) || true !== $settings['show_preview'];

		foreach ( $posts as $post ) {
			// If already marked as PDF result, ensure PDF data is available.
			if ( isset( $post->is_pdf_result ) && $post->is_pdf_result ) {
				// Add PDF data to post object if not already set.
				if ( ! isset( $post->pdf_data ) ) {
					$indexed_data = Database::get_indexed_pdf( $post->ID );
					if ( $indexed_data ) {
						$post->pdf_data = $indexed_data;
					}
				}
			} elseif ( 'attachment' === $post->post_type ) {
				// Check if it's a PDF attachment and mark it.
				$mime_type = get_post_mime_type( $post->ID );
				if ( $mime_type && 'application/pdf' === $mime_type ) {
					$indexed_data = Database::get_indexed_pdf( $post->ID );
					if ( $indexed_data ) {
						$post->is_pdf_result = true;
						$post->pdf_data      = $indexed_data;
					}
				}
			}

			// When preview is disabled, strip _thumbnail_id from the meta cache so no theme
			// can render a preview image, regardless of which function it uses.
			if ( $hide_preview && isset( $post->is_pdf_result ) && $post->is_pdf_result ) {
				$meta_cache = wp_cache_get( $post->ID, 'post_meta' );
				if ( is_array( $meta_cache ) && isset( $meta_cache['_thumbnail_id'] ) ) {
					unset( $meta_cache['_thumbnail_id'] );
					wp_cache_set( $post->ID, $meta_cache, 'post_meta' );
				}
			}
		}

		return $posts;
	}

	/**
	 * Search PDFs in the index
	 *
	 * @since 1.0.0
	 * @param string    $search_term Search query.
	 * @param \WP_Query $query WordPress query object (unused but required by filter signature).
	 * @return array Array of post objects
	 */
	private function search_pdfs( $search_term, $query ) {
		// Parameter is required by filter signature but unused here.
		$query = null;
		unset( $query );

		$results = Database::search_index( $search_term );

		if ( empty( $results ) ) {
			return array();
		}

		// Convert to post objects.
		$posts = array();
		foreach ( $results as $result ) {
			$post = get_post( $result->post_id );

			if ( ! $post ) {
				continue;
			}

			// Ensure post has proper permalink.
			$post->post_type   = 'attachment';
			$post->post_status = 'inherit';

			// Set permalink to PDF URL.
			if ( ! empty( $result->pdf_url ) ) {
				$post->guid = $result->pdf_url;
			}

			// Add PDF-specific properties.
			$post->is_pdf_result   = true;
			$title_bonus           = isset( $result->title_bonus ) ? (float) $result->title_bonus : 0;
			$post->relevance_score = (float) $result->relevance_score + $title_bonus;
			$post->pdf_data        = $result;

			// Generate excerpt with highlighted search terms.
			$settings       = Database::get_settings();
			$excerpt_length = $settings['excerpt_length'];
			$content_for_excerpt = isset( $result->matched_page_content ) && '' !== trim( (string) $result->matched_page_content )
				? $result->matched_page_content
				: $result->pdf_content;
			$excerpt             = $this->generate_excerpt( $content_for_excerpt, $search_term, $excerpt_length );

			// Build plain-text excerpt with meta prefix for themes that strip HTML (Astra, Divi, etc.).
			$plain_meta = array();
			if ( ! empty( $settings['show_author'] ) ) {
				$author_id = (int) $post->post_author;
				if ( $author_id > 0 ) {
					$author_name = get_the_author_meta( 'display_name', $author_id );
					if ( '' !== $author_name ) {
						$plain_meta[] = __( 'Author', 'webequipe-pdf-search' ) . ': ' . $author_name;
					}
				}
			}
			if ( ! empty( $settings['show_file_size'] ) && ! empty( $result->pdf_filesize ) ) {
				$plain_meta[] = __( 'Size', 'webequipe-pdf-search' ) . ': ' . size_format( $result->pdf_filesize );
			}
			if ( ! empty( $settings['show_page_count'] ) && isset( $result->page_count ) && (int) $result->page_count > 0 ) {
				$plain_meta[] = __( 'Page', 'webequipe-pdf-search' ) . ': ' . $result->page_count;
			}
			if ( ! empty( $settings['show_modified_date'] ) ) {
				$date_val = get_the_modified_date( '', $post->ID );
				if ( $date_val ) {
					$plain_meta[] = __( 'Date', 'webequipe-pdf-search' ) . ': ' . $date_val;
				}
			}
			$plain_prefix       = ! empty( $plain_meta ) ? implode( ' | ', $plain_meta ) . ' — ' : '';
			$plain_excerpt      = wp_strip_all_tags( $excerpt );
			$post->post_excerpt = $plain_prefix . $plain_excerpt;

			// Build content for themes (e.g. Avada) that read post_content directly.
			$post->post_content = $this->build_pdf_content( $post, $result, $settings, $search_term );

			$posts[] = $post;
		}

		return $posts;
	}

	/**
	 * Build HTML content for a PDF result (meta line + preview + excerpt).
	 * Stored in post_content so themes that read it directly (e.g. Avada) show the data.
	 *
	 * @since 1.0.1
	 * @param \WP_Post $post        Post object.
	 * @param object   $pdf_data    Indexed PDF row.
	 * @param array    $settings    Plugin settings.
	 * @param string   $search_term Current search query.
	 * @return string HTML content.
	 */
	private function build_pdf_content( $post, $pdf_data, $settings, $search_term ) {
		$parts = array();

		// Meta line.
		$meta_items = array();
		if ( ! empty( $settings['show_author'] ) ) {
			$author_id = (int) $post->post_author;
			if ( $author_id > 0 ) {
				$author_name = get_the_author_meta( 'display_name', $author_id );
				if ( '' !== $author_name ) {
					$meta_items[] = esc_html__( 'Author', 'webequipe-pdf-search' ) . ': ' . esc_html( $author_name );
				}
			}
		}
		if ( ! empty( $settings['show_file_size'] ) && $pdf_data && ! empty( $pdf_data->pdf_filesize ) ) {
			$meta_items[] = esc_html__( 'Size', 'webequipe-pdf-search' ) . ': ' . esc_html( size_format( $pdf_data->pdf_filesize ) );
		}
		if ( ! empty( $settings['show_page_count'] ) && $pdf_data && isset( $pdf_data->page_count ) && (int) $pdf_data->page_count > 0 ) {
			$meta_items[] = esc_html__( 'Page', 'webequipe-pdf-search' ) . ': ' . esc_html( $pdf_data->page_count );
		}
		if ( ! empty( $settings['show_modified_date'] ) ) {
			$date_value = get_the_modified_date( '', $post->ID );
			if ( $date_value ) {
				$meta_items[] = esc_html__( 'Date', 'webequipe-pdf-search' ) . ': ' . esc_html( $date_value );
			}
		}
		if ( ! empty( $meta_items ) ) {
			$parts[] = '<div class="webequipe-pdf-meta"><span class="webequipe-pdf-meta-item">' . implode( '</span> <span class="webequipe-pdf-meta-sep">|</span> <span class="webequipe-pdf-meta-item">', $meta_items ) . '</span></div>';
		}

		// Preview.
		if ( ! empty( $settings['show_preview'] ) ) {
			$preview_url = \WebEquipe\PDFSearch\webequipe_pdf_search_get_preview_image_url( $post->ID );
			if ( '' !== $preview_url ) {
				$parts[] = '<div class="webequipe-pdf-preview-wrapper"><img class="webequipe-pdf-preview" src="' . esc_url( $preview_url ) . '" alt="' . esc_attr__( 'PDF preview', 'webequipe-pdf-search' ) . '" loading="lazy" decoding="async" /></div>';
			}
		}

		// Summary excerpt.
		if ( ! empty( $settings['show_summary'] ) && $pdf_data && ! empty( $pdf_data->pdf_content ) ) {
			$excerpt_text = $this->generate_excerpt( $pdf_data->pdf_content, $search_term, (int) $settings['excerpt_length'] );
			if ( '' !== trim( (string) $excerpt_text ) ) {
				$parts[] = '<div class="webequipe-pdf-excerpt">' . $excerpt_text . '</div>';
			}
		}

		if ( empty( $parts ) ) {
			return '';
		}

		return '<div class="webequipe-pdf-search-result-content">' . implode( '', $parts ) . '</div>';
	}

	/**
	 * Generate excerpt with highlighted search terms
	 *
	 * @since 1.0.0
	 * @param string $content PDF content.
	 * @param string $search_term Search query.
	 * @param int    $length Excerpt length.
	 * @return string Excerpt with highlighted terms
	 */
	private function generate_excerpt( $content, $search_term, $length = 200 ) {
		// Find first occurrence of search term.
		$search_words   = explode( ' ', $search_term );
		$found_position = false;

		foreach ( $search_words as $word ) {
			$pos = stripos( $content, $word );
			if ( false !== $pos ) {
				$found_position = $pos;
				break;
			}
		}

		if ( false === $found_position ) {
			$found_position = 0;
		}

		// Extract excerpt around found position.
		$start   = max( 0, $found_position - ( $length / 2 ) );
		$excerpt = substr( $content, $start, $length );

		if ( $start > 0 ) {
			$excerpt = '...' . $excerpt;
		}

		if ( strlen( $content ) > $start + $length ) {
			$excerpt .= '...';
		}

		// Highlight search terms.
		foreach ( $search_words as $word ) {
			$excerpt = preg_replace(
				'/' . preg_quote( $word, '/' ) . '/i',
				'<mark>' . esc_html( $word ) . '</mark>',
				$excerpt
			);
		}

		return $excerpt;
	}

	/**
	 * Modify PDF title in search results
	 *
	 * @since 1.0.0
	 * @param string $title Post title.
	 * @param int    $post_id Post ID (unused but required by filter signature).
	 * @return string Modified title
	 */
	public function modify_pdf_title( $title, $post_id = null ) {
		if ( ! is_search() ) {
			return $title;
		}

		global $post;
		$current_post = $post;

		// If post_id is provided, get that post instead.
		if ( $post_id ) {
			$current_post = get_post( $post_id );
			if ( ! $current_post ) {
				return $title;
			}
		}

		// If no post available, return early.
		if ( ! $current_post ) {
			return $title;
		}

		// Check if this is a PDF result.
		$is_pdf_result = false;
		if ( isset( $current_post->is_pdf_result ) && $current_post->is_pdf_result ) {
			$is_pdf_result = true;
		} elseif ( 'attachment' === $current_post->post_type ) {
			// Fallback: Check if it's a PDF attachment and indexed.
			$mime_type = get_post_mime_type( $current_post->ID );
			if ( $mime_type && 'application/pdf' === $mime_type ) {
				$indexed = Database::get_indexed_pdf( $current_post->ID );
				if ( $indexed ) {
					$is_pdf_result               = true;
					$current_post->is_pdf_result = true;
					$current_post->pdf_data      = $indexed;
				}
			}
		}

		if ( ! $is_pdf_result ) {
			return $title;
		}

		$settings = Database::get_settings();

		// Make title friendlier for display: replace underscores with spaces, remove .pdf extension.
		$title = $this->get_display_title( $title );

		// If PDF icon is enabled, prepend icon before the title so it appears
		// consistently across themes (classic and block-based).
		$escaped_title = esc_html( $title );
		$title         = $escaped_title;
		if ( ! empty( $settings['show_pdf_icon'] ) && true === $settings['show_pdf_icon'] ) {
			$icon = \WebEquipe\PDFSearch\webequipe_pdf_search_get_icon();
			if ( ! empty( $icon ) ) {
				$title = '<span class="webequipe-pdf-title-line"><span class="webequipe-pdf-icon webequipe-pdf-icon--inline">' . $icon . '</span> ' . $escaped_title . '</span>';
			}
		}

		$allowed = array(
			'span' => array(
				'class' => true,
			),
			'img'  => array(
				'class' => true,
				'src'   => true,
				'alt'   => true,
			),
		);

		return wp_kses( $title, $allowed );
	}

	/**
	 * Get a display-friendly title for a PDF (replace underscores, strip .pdf).
	 *
	 * @since 1.0.1
	 * @param string $title Raw title (e.g. attachment title or filename).
	 * @return string
	 */
	private function get_display_title( $title ) {
		return \WebEquipe\PDFSearch\webequipe_pdf_search_get_display_title( $title );
	}

	/**
	 * Modify PDF excerpt in search results
	 *
	 * @since 1.0.0
	 * @param string   $excerpt Post excerpt.
	 * @param \WP_Post $post Post object.
	 * @return string Modified excerpt
	 */
	public function modify_pdf_excerpt( $excerpt, $post = null ) {
		// Use parameter if provided, otherwise use global.
		$current_post = $post;
		if ( ! $current_post ) {
			global $post;
			$current_post = $post;
		}
		// Block themes / some contexts pass post ID; ensure we have an object.
		if ( is_numeric( $current_post ) ) {
			$current_post = get_post( (int) $current_post );
		}

		// If no post available, return early.
		if ( ! $current_post || ! ( $current_post instanceof \WP_Post ) ) {
			return $excerpt;
		}

		// Pass-through: if the input already contains our wrapper (from an earlier filter in the same chain), return as-is.
		if ( is_string( $excerpt ) && false !== strpos( $excerpt, 'webequipe-pdf-search-result-content' ) ) {
			return $excerpt;
		}

		// If the content path already rendered for this post, suppress to avoid duplicate.
		if ( isset( self::$rendered_ids[ $current_post->ID ] ) ) {
			return '';
		}

		// Check if this is a PDF result.
		$is_pdf_result = false;
		$pdf_data      = null;

		if ( isset( $current_post->is_pdf_result ) && $current_post->is_pdf_result ) {
			$is_pdf_result = true;
			$pdf_data      = isset( $current_post->pdf_data ) ? $current_post->pdf_data : null;
			// Block themes may pass a post without pdf_data; load from DB.
			if ( ! $pdf_data && ! empty( $current_post->ID ) ) {
				$pdf_data               = Database::get_indexed_pdf( $current_post->ID );
				$current_post->pdf_data = $pdf_data;
			}
		} elseif ( 'attachment' === $current_post->post_type ) {
			// Fallback: Check if it's a PDF attachment.
			$mime_type = get_post_mime_type( $current_post->ID );
			if ( $mime_type && 'application/pdf' === $mime_type ) {
				$indexed = Database::get_indexed_pdf( $current_post->ID );
				if ( $indexed ) {
					$is_pdf_result               = true;
					$current_post->is_pdf_result = true;
					$current_post->pdf_data      = $indexed;
					$pdf_data                    = $indexed;
				}
			}
		}

		if ( ! $is_pdf_result ) {
			return $excerpt;
		}

		// Only output for search, or when excerpt would otherwise be empty (so block themes using only Post Excerpt block still show meta). Never in admin.
		if ( is_admin() ) {
			return $excerpt;
		}
		$is_search_context = is_search();
		if ( ! $is_search_context && get_search_query() !== '' ) {
			$is_search_context = true;
		}
		$excerpt_empty = ( ! is_string( $excerpt ) || trim( $excerpt ) === '' );
		if ( ! $is_search_context && ! $excerpt_empty ) {
			return $excerpt;
		}

		$settings = Database::get_settings();
		$output   = '';

		// Build meta line: author (uploader), size, pages, date (PDF icon is in title).
		$meta_items = array();

		if ( ! empty( $settings['show_author'] ) && $settings['show_author'] ) {
			$author_id = (int) $current_post->post_author;
			if ( $author_id > 0 ) {
				$author_name = get_the_author_meta( 'display_name', $author_id );
				if ( '' !== $author_name ) {
					$meta_items[] = '<span class="webequipe-pdf-meta-item">' . esc_html__( 'Author', 'webequipe-pdf-search' ) . ': ' . esc_html( $author_name ) . '</span>';
				}
			}
		}

		if ( ! empty( $settings['show_file_size'] ) && $settings['show_file_size'] && $pdf_data ) {
			$file_size = size_format( $pdf_data->pdf_filesize );
			if ( $file_size ) {
				$meta_items[] = '<span class="webequipe-pdf-meta-item">' . esc_html__( 'Size', 'webequipe-pdf-search' ) . ': ' . esc_html( $file_size ) . '</span>';
			}
		}

		if ( ! empty( $settings['show_page_count'] ) && $settings['show_page_count'] && $pdf_data && isset( $pdf_data->page_count ) ) {
			$page_count = (int) $pdf_data->page_count;
			if ( $page_count > 0 ) {
				$meta_items[] = '<span class="webequipe-pdf-meta-item">' . esc_html__( 'Page', 'webequipe-pdf-search' ) . ': ' . esc_html( $page_count ) . '</span>';
			}
		}

		if ( ! empty( $settings['show_modified_date'] ) && $settings['show_modified_date'] ) {
			$date_value = get_the_modified_date( '', $current_post->ID );
			if ( $date_value ) {
				$meta_items[] = '<span class="webequipe-pdf-meta-item">' . esc_html__( 'Date', 'webequipe-pdf-search' ) . ': ' . esc_html( $date_value ) . '</span>';
			}
		}

		if ( ! empty( $meta_items ) ) {
			$meta_line = implode( ' <span class="webequipe-pdf-meta-sep">|</span> ', $meta_items );
			$output   .= '<span class="webequipe-pdf-meta">' . $meta_line . '</span>';
		}

		// Optional preview thumbnail (after meta). Only show when a real image exists, not a transparent placeholder.
		if ( ! empty( $settings['show_preview'] ) && true === $settings['show_preview'] ) {
			$preview_url = \WebEquipe\PDFSearch\webequipe_pdf_search_get_preview_image_url( $current_post->ID );
			if ( '' !== $preview_url ) {
				$thumbnail = '<img class="webequipe-pdf-preview" src="' . esc_url( $preview_url ) . '" alt="' . esc_attr__( 'PDF preview', 'webequipe-pdf-search' ) . '" loading="lazy" decoding="async" />';
				$output   .= '<span class="webequipe-pdf-preview-wrapper">' . $thumbnail . '</span>';
			}
		}

		// Add excerpt/summary text if enabled and available.
		$show_summary = ! empty( $settings['show_summary'] );
		if ( $show_summary ) {
			$excerpt_text = '';
			if ( ! empty( $current_post->post_excerpt ) ) {
				$excerpt_text = $current_post->post_excerpt;
				// We already output meta above in .webequipe-pdf-meta; post_excerpt was set with a
				// plain-text meta prefix for themes that strip HTML. Strip it here to avoid duplicate meta.
				$sep = ' — ';
				if ( strpos( $excerpt_text, $sep ) !== false ) {
					$excerpt_text = substr( $excerpt_text, strpos( $excerpt_text, $sep ) + strlen( $sep ) );
				}
			}
			if ( ( '' === trim( (string) $excerpt_text ) ) && $pdf_data && isset( $pdf_data->pdf_content ) && '' !== trim( (string) $pdf_data->pdf_content ) ) {
				$excerpt_text = $this->generate_excerpt( $pdf_data->pdf_content, get_search_query(), (int) $settings['excerpt_length'] );
			}
			// Fallback: if still empty but we have content, use start of content so preview always shows.
			if ( ( '' === trim( (string) $excerpt_text ) ) && $pdf_data && isset( $pdf_data->pdf_content ) && '' !== trim( (string) $pdf_data->pdf_content ) ) {
				$len          = (int) $settings['excerpt_length'];
				$raw          = wp_strip_all_tags( $pdf_data->pdf_content );
				$raw          = preg_replace( '/\s+/', ' ', trim( $raw ) );
				$excerpt_text = ( strlen( $raw ) > $len ) ? substr( $raw, 0, $len ) . '...' : $raw;
			}
			// When no extractable text (e.g. image-only PDF or empty index), show a short label so preview block still appears.
			if ( '' === trim( (string) $excerpt_text ) ) {
				$excerpt_text = esc_html__( 'PDF document.', 'webequipe-pdf-search' );
			}
			if ( '' !== trim( (string) $excerpt_text ) ) {
				$output .= '<span class="webequipe-pdf-excerpt">' . $excerpt_text . '</span>';
			}
		}

		// Wrap in container. Use span so output is valid inside block theme's <p class="wp-block-post-excerpt__excerpt">.
		if ( '' !== $output ) {
			$output = '<span class="webequipe-pdf-search-result-content">' . $output . '</span>';
		}

		$allowed = array(
			'span' => array(
				'class' => true,
			),
			'div'  => array(
				'class' => true,
			),
			'mark' => array(),
			'img'  => array(
				'class'       => true,
				'src'         => true,
				'alt'         => true,
				'srcset'      => true,
				'sizes'       => true,
				'loading'     => true,
				'decoding'    => true,
				'width'       => true,
				'height'      => true,
				'data-src'    => true,
				'data-srcset' => true,
				'data-sizes'  => true,
			),
		);

		// Mark this post as rendered so other filter paths (the_content / Elementor) won't duplicate.
		self::$rendered_ids[ $current_post->ID ] = true;

		return wp_kses( $output, $allowed, array( 'http', 'https', 'data' ) );
	}

	/**
	 * Modify trimmed excerpt output for PDF results.
	 *
	 * @since 1.0.0
	 * @param string $text The trimmed excerpt.
	 * @param string $raw_excerpt The raw excerpt text.
	 * @return string Modified excerpt
	 */
	public function modify_pdf_trim_excerpt( $text, $raw_excerpt ) {
		unset( $raw_excerpt );
		return $this->modify_pdf_excerpt( $text, null );
	}

	/**
	 * Prevent wp_trim_words from stripping our PDF excerpt HTML (meta, preview, summary).
	 * The core Post Excerpt block trims with wp_trim_words(), which strips all tags; this keeps our output intact.
	 *
	 * @since 1.0.1
	 * @param string $text         Trimmed text.
	 * @param int    $num_words    Number of words.
	 * @param string $more         More link text.
	 * @param string $original_text Original text before trimming.
	 * @return string
	 */
	public function preserve_pdf_excerpt_html( $text, $num_words, $more, $original_text ) {
		if ( is_string( $original_text ) && false !== strpos( $original_text, 'webequipe-pdf-search-result-content' ) ) {
			return $original_text;
		}
		return $text;
	}

	/**
	 * Modify rendered excerpt output for themes that call the_excerpt() directly.
	 *
	 * @since 1.0.1
	 * @param string $excerpt Excerpt HTML/text.
	 * @return string Modified excerpt
	 */
	public function modify_pdf_rendered_excerpt( $excerpt ) {
		return $this->modify_pdf_excerpt( $excerpt, null );
	}

	/**
	 * Modify PDF content in search results.
	 *
	 * @since 1.0.0
	 * @param string $content Post content.
	 * @return string Modified content
	 */
	public function modify_pdf_content( $content ) {
		if ( ! is_search() || ! in_the_loop() ) {
			return $content;
		}

		global $post;
		if ( ! $post ) {
			return $content;
		}

		// Only handle PDF results; leave regular posts untouched.
		if ( ! isset( $post->is_pdf_result ) || ! $post->is_pdf_result ) {
			return $content;
		}

		// If the excerpt path already rendered for this post, suppress to avoid duplicate.
		if ( isset( self::$rendered_ids[ $post->ID ] ) ) {
			return '';
		}

		// Mark rendered so the excerpt path won't produce a second copy.
		self::$rendered_ids[ $post->ID ] = true;

		// Strip WordPress core's prepend_attachment output (<p class="attachment">…</p>)
		// which adds a duplicate thumbnail for attachment posts.
		if ( is_string( $content ) ) {
			$content = preg_replace( '/<p\s+class="attachment">.*?<\/p>\s*/s', '', $content );
		}

		// If content already contains our wrapper (pre-built by build_pdf_content), use it.
		if ( is_string( $content ) && false !== strpos( $content, 'webequipe-pdf-search-result-content' ) ) {
			return $content;
		}

		// Build fresh.
		return $this->modify_pdf_excerpt( '', $post );
	}

	/**
	 * Universal block-theme safety net: append PDF content after any core post block
	 * when no other hook (the_content, get_the_excerpt) has rendered it yet.
	 *
	 * Targets core blocks present in every block theme search template.
	 * Only injects once per post (tracked by $rendered_ids).
	 *
	 * @since 1.0.1
	 * @param string $block_content Rendered block HTML.
	 * @param array  $block         Block data.
	 * @return string
	 */
	public function maybe_append_pdf_content_to_block( $block_content, $block ) {
		if ( ! isset( $block['blockName'] ) ) {
			return $block_content;
		}

		// Only act on core post blocks that appear in search results.
		$target_blocks = array(
			'core/post-title',
			'core/post-date',
			'core/post-excerpt',
			'core/post-content',
			'core/post-author',
			'core/post-author-name',
		);
		if ( ! in_array( $block['blockName'], $target_blocks, true ) ) {
			return $block_content;
		}

		// Only on front-end search.
		if ( is_admin() ) {
			return $block_content;
		}
		$is_search_context = is_search() || get_search_query() !== '';
		if ( ! $is_search_context ) {
			return $block_content;
		}

		global $post;
		if ( ! $post || ! ( $post instanceof \WP_Post ) ) {
			return $block_content;
		}

		// Already rendered by the_content, get_the_excerpt, or a previous block.
		if ( isset( self::$rendered_ids[ $post->ID ] ) ) {
			return $block_content;
		}

		// Check if it's a PDF result.
		if ( ! isset( $post->is_pdf_result ) || ! $post->is_pdf_result ) {
			if ( 'attachment' !== $post->post_type ) {
				return $block_content;
			}
			$mime_type = get_post_mime_type( $post->ID );
			if ( ! $mime_type || 'application/pdf' !== $mime_type ) {
				return $block_content;
			}
			$indexed = Database::get_indexed_pdf( $post->ID );
			if ( ! $indexed ) {
				return $block_content;
			}
			$post->is_pdf_result = true;
			$post->pdf_data      = $indexed;
		}

		// Build and append after this block.
		$pdf_html = $this->modify_pdf_excerpt( '', $post );
		if ( '' === $pdf_html ) {
			return $block_content;
		}

		return $block_content . $pdf_html;
	}

	/**
	 * Intercept _thumbnail_id meta reads for PDF attachments in search when preview is off.
	 *
	 * This is the lowest-level filter — it catches get_post_meta(), get_post_thumbnail_id(),
	 * has_post_thumbnail(), and any theme code that reads _thumbnail_id directly.
	 *
	 * @since 1.0.1
	 * @param mixed  $value    Null to use default, non-null to short-circuit.
	 * @param int    $post_id  Post ID.
	 * @param string $meta_key Meta key being read.
	 * @param bool   $single   Whether single value requested.
	 * @return mixed
	 */
	public function hide_pdf_thumbnail_meta_when_preview_disabled( $value, $post_id, $meta_key, $single ) {
		if ( '_thumbnail_id' !== $meta_key ) {
			return $value;
		}
		if ( is_admin() ) {
			return $value;
		}
		if ( ! is_search() && '' === get_search_query() ) {
			return $value;
		}
		if ( 'application/pdf' !== get_post_mime_type( $post_id ) ) {
			return $value;
		}
		$settings = Database::get_settings();
		if ( ! empty( $settings['show_preview'] ) && true === $settings['show_preview'] ) {
			return $value;
		}
		return $single ? false : array();
	}

	/**
	 * Return empty HTML for the post thumbnail when PDF preview is disabled in search.
	 *
	 * Catches themes using get_the_post_thumbnail().
	 *
	 * @since 1.0.1
	 * @param string       $html              Thumbnail HTML.
	 * @param int          $post_id            Post ID.
	 * @param int          $post_thumbnail_id  Thumbnail attachment ID.
	 * @param string|int[] $size               Image size.
	 * @param string|array $attr               Attributes.
	 * @return string
	 */
	public function hide_pdf_thumbnail_html_when_preview_disabled( $html, $post_id, $post_thumbnail_id, $size, $attr ) {
		if ( is_admin() || $post_id <= 0 ) {
			return $html;
		}
		if ( ! is_search() && '' === get_search_query() ) {
			return $html;
		}
		if ( 'application/pdf' !== get_post_mime_type( $post_id ) ) {
			return $html;
		}
		$settings = Database::get_settings();
		if ( ! empty( $settings['show_preview'] ) && true === $settings['show_preview'] ) {
			return $html;
		}
		return '';
	}

	/**
	 * Return empty HTML for wp_get_attachment_image() when the attachment is a PDF (or
	 * the auto-generated JPEG preview of a PDF) and preview is disabled in search.
	 *
	 * @since 1.0.1
	 * @param string       $html          Image HTML.
	 * @param int          $attachment_id Attachment ID.
	 * @param string|int[] $size          Image size.
	 * @param bool         $icon          Whether to use icon.
	 * @param string[]     $attr          Attributes.
	 * @return string
	 */
	public function hide_pdf_attachment_image_when_preview_disabled( $html, $attachment_id, $size, $icon, $attr ) {
		if ( is_admin() || $attachment_id <= 0 ) {
			return $html;
		}
		if ( ! is_search() && '' === get_search_query() ) {
			return $html;
		}

		$is_pdf = ( 'application/pdf' === get_post_mime_type( $attachment_id ) );

		// Also check if this is the auto-generated JPEG preview of a PDF attachment.
		// WordPress stores the JPEG as a child of the PDF, or the PDF references it
		// via _thumbnail_id. Check if the parent post is a PDF.
		if ( ! $is_pdf ) {
			$parent_id = wp_get_post_parent_id( $attachment_id );
			if ( $parent_id > 0 && 'application/pdf' === get_post_mime_type( $parent_id ) ) {
				$is_pdf = true;
			}
		}

		if ( ! $is_pdf ) {
			return $html;
		}
		$settings = Database::get_settings();
		if ( ! empty( $settings['show_preview'] ) && true === $settings['show_preview'] ) {
			return $html;
		}
		return '';
	}

	/**
	 * Modify PDF permalink to point to PDF file
	 *
	 * @since 1.0.0
	 * @param string   $permalink Post permalink.
	 * @param \WP_Post $post Post object.
	 * @return string Modified permalink
	 */
	public function modify_pdf_permalink( $permalink, $post ) {
		if ( ! is_search() || ! isset( $post->is_pdf_result ) || ! $post->is_pdf_result ) {
			return $permalink;
		}

		if ( isset( $post->pdf_data ) && ! empty( $post->pdf_data->pdf_url ) ) {
			return $post->pdf_data->pdf_url;
		}

		// Fallback to attachment URL.
		return wp_get_attachment_url( $post->ID );
	}
}
