<?php
/**
 * Non-destructive FAQ/HowTo content-to-block suggestion detector.
 *
 * Prefer Ultra Writer structured rich-block configs over prose heuristics.
 * Never emits schema. Never treats a title alone (e.g. "FAQ about …") as a block.
 *
 * @package SEOvaultSEO\Schema\Content
 */

namespace SEOvaultSEO\Schema\Content;

use SEOvaultSEO\Blocks\FaqBlock;
use SEOvaultSEO\Blocks\FaqBlockAttributeSanitizer;
use SEOvaultSEO\Blocks\HowToBlock;
use SEOvaultSEO\Blocks\HowToBlockAttributeSanitizer;
use SEOvaultSEO\Blocks\LegacyFaqRichBlockConverter;

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

/**
 * Detects candidate FAQ/HowTo conversions as suggestions only.
 */
class SchemaContentSuggestionDetector {

	const KIND_FAQ   = 'faq';
	const KIND_HOWTO = 'howto';

	const SOURCE_ULTRA = 'ultra_structured';
	const SOURCE_PROSE = 'prose_heuristic';

	const CONFIDENCE_HIGH   = 'high';
	const CONFIDENCE_MEDIUM = 'medium';

	/**
	 * Soft scan budget (bytes of post content considered).
	 */
	const MAX_SCAN_BYTES = 200000;

	/**
	 * Detect suggestions for a post.
	 *
	 * @param int         $post_id Post ID.
	 * @param string|null $content Optional content override (editor draft).
	 * @param string|null $title   Optional title override.
	 * @return array{content_hash:string,suggestions:array<int,array>}
	 */
	public static function detect_for_post( $post_id, $content = null, $title = null ) {
		$post_id = (int) $post_id;
		$post    = $post_id > 0 ? get_post( $post_id ) : null;
		if ( null === $content ) {
			$content = $post ? (string) $post->post_content : '';
		}
		if ( null === $title ) {
			$title = $post ? (string) $post->post_title : '';
		}
		$content = is_string( $content ) ? $content : '';
		$title   = is_string( $title ) ? $title : '';
		if ( strlen( $content ) > self::MAX_SCAN_BYTES ) {
			$content = substr( $content, 0, self::MAX_SCAN_BYTES );
		}

		$hash = hash( 'sha256', $content );
		$out  = array();

		if ( ! self::content_has_native_block( $content, FaqBlock::BLOCK_NAME ) ) {
			$faq = self::detect_faq( $content, $title );
			if ( is_array( $faq ) && ! SchemaContentSuggestionStore::is_dismissed( $post_id, self::KIND_FAQ, $hash ) ) {
				$out[] = $faq;
			}
		}

		if ( ! self::content_has_native_block( $content, HowToBlock::BLOCK_NAME ) ) {
			$howto = self::detect_howto( $content, $title );
			if ( is_array( $howto ) && ! SchemaContentSuggestionStore::is_dismissed( $post_id, self::KIND_HOWTO, $hash ) ) {
				$out[] = $howto;
			}
		}

		return array(
			'content_hash' => $hash,
			'suggestions'  => $out,
		);
	}

	/**
	 * Whether content already has a native block of the given name.
	 *
	 * @param string $content    Post content.
	 * @param string $block_name Block name.
	 * @return bool
	 */
	public static function content_has_native_block( $content, $block_name ) {
		$content    = is_string( $content ) ? $content : '';
		$block_name = (string) $block_name;
		if ( '' === $content || '' === $block_name ) {
			return false;
		}
		$needle = '<!-- wp:' . $block_name;
		if ( false !== strpos( $content, $needle ) ) {
			return true;
		}
		if ( ! function_exists( 'parse_blocks' ) ) {
			return false;
		}
		return self::blocks_contain( \SEOvaultSEO\Schema\SchemaBlockParseCache::parse( $content ), $block_name );
	}

	/**
	 * Sanitize and validate user-edited suggestion attributes before insert.
	 *
	 * @param string $kind       faq|howto.
	 * @param array  $attributes Proposed attributes.
	 * @return array{ok:bool,kind:string,block_name:string,attributes:array,errors:array,preview:array}
	 */
	public static function sanitize_confirmation( $kind, array $attributes ) {
		$kind = sanitize_key( (string) $kind );
		if ( self::KIND_FAQ === $kind ) {
			$sanitized = FaqBlockAttributeSanitizer::sanitize( $attributes );
			$complete  = FaqBlockAttributeSanitizer::get_complete_items( $sanitized['attributes'] );
			$ok        = empty( array_intersect( $sanitized['errors'], array( 'payload_too_large', 'attributes_not_array' ) ) )
				&& count( $complete ) >= 1;
			return array(
				'ok'         => $ok,
				'kind'       => self::KIND_FAQ,
				'block_name' => FaqBlock::BLOCK_NAME,
				'attributes' => self::client_safe_faq_attributes( $sanitized['attributes'] ),
				'errors'     => $ok ? array() : ( empty( $complete ) ? array( 'incomplete_faq' ) : $sanitized['errors'] ),
				'preview'    => self::faq_preview_rows( $sanitized['attributes'] ),
			);
		}
		if ( self::KIND_HOWTO === $kind ) {
			$sanitized = HowToBlockAttributeSanitizer::sanitize( $attributes );
			$complete  = HowToBlockAttributeSanitizer::get_complete_steps( $sanitized['attributes'] );
			$name      = isset( $sanitized['attributes']['name'] ) ? trim( (string) $sanitized['attributes']['name'] ) : '';
			$ok        = empty( array_intersect( $sanitized['errors'], array( 'payload_too_large', 'attributes_not_array' ) ) )
				&& '' !== $name
				&& count( $complete ) >= 1;
			return array(
				'ok'         => $ok,
				'kind'       => self::KIND_HOWTO,
				'block_name' => HowToBlock::BLOCK_NAME,
				'attributes' => self::client_safe_howto_attributes( $sanitized['attributes'] ),
				'errors'     => $ok ? array() : ( '' === $name || empty( $complete ) ? array( 'incomplete_howto' ) : $sanitized['errors'] ),
				'preview'    => self::howto_preview_rows( $sanitized['attributes'] ),
			);
		}
		return array(
			'ok'         => false,
			'kind'       => $kind,
			'block_name' => '',
			'attributes' => array(),
			'errors'     => array( 'unknown_kind' ),
			'preview'    => array(),
		);
	}

	/**
	 * Detect FAQ suggestion.
	 *
	 * @param string $content Content.
	 * @param string $title   Title.
	 * @return array|null
	 */
	private static function detect_faq( $content, $title ) {
		// Title-only false positive: never suggest from the post title alone.
		if ( self::looks_like_faq_title( $title ) && self::is_effectively_empty_body( $content ) ) {
			return null;
		}

		$ultra = self::detect_ultra_faq( $content );
		if ( is_array( $ultra ) ) {
			return $ultra;
		}

		return self::detect_prose_faq( $content, $title );
	}

	/**
	 * Detect HowTo suggestion.
	 *
	 * @param string $content Content.
	 * @param string $title   Title.
	 * @return array|null
	 */
	private static function detect_howto( $content, $title ) {
		if ( self::looks_like_howto_title( $title ) && self::is_effectively_empty_body( $content ) ) {
			return null;
		}

		$ultra = self::detect_ultra_howto( $content );
		if ( is_array( $ultra ) ) {
			return $ultra;
		}

		return self::detect_prose_howto( $content, $title );
	}

	/**
	 * Ultra Writer FAQ rich-block suggestion.
	 *
	 * @param string $content Content.
	 * @return array|null
	 */
	private static function detect_ultra_faq( $content ) {
		if ( ! LegacyFaqRichBlockConverter::content_has_legacy( $content ) ) {
			return null;
		}
		$detection = LegacyFaqRichBlockConverter::detect( $content );
		if ( empty( $detection['convertible_count'] ) ) {
			return null;
		}
		$match = null;
		foreach ( $detection['matches'] as $row ) {
			if ( is_array( $row ) && ! empty( $row['convertible'] ) && ! empty( $row['attributes'] ) ) {
				$match = $row;
				break;
			}
		}
		if ( ! is_array( $match ) ) {
			return null;
		}
		$attrs    = is_array( $match['attributes'] ) ? $match['attributes'] : array();
		$complete = FaqBlockAttributeSanitizer::get_complete_items( $attrs );
		if ( count( $complete ) < 1 ) {
			return null;
		}
		return self::build_suggestion(
			self::KIND_FAQ,
			self::SOURCE_ULTRA,
			self::CONFIDENCE_HIGH,
			__( 'Structured FAQ from Ultra Writer / web rich block.', 'seovault-ai' ),
			FaqBlock::BLOCK_NAME,
			self::client_safe_faq_attributes( $attrs ),
			self::faq_preview_rows( $attrs )
		);
	}

	/**
	 * Ultra Writer HowTo rich-block suggestion.
	 *
	 * @param string $content Content.
	 * @return array|null
	 */
	private static function detect_ultra_howto( $content ) {
		if ( ! preg_match( '/data-seovault-rich-block\s*=\s*(["\'])how-to\1/i', $content ) ) {
			return null;
		}
		$configs = self::extract_rich_block_configs( $content, 'how-to' );
		foreach ( $configs as $raw ) {
			$mapped = self::map_howto_rich_config( $raw );
			if ( empty( $mapped['ok'] ) ) {
				continue;
			}
			return self::build_suggestion(
				self::KIND_HOWTO,
				self::SOURCE_ULTRA,
				self::CONFIDENCE_HIGH,
				__( 'Structured How-to from Ultra Writer / web rich block.', 'seovault-ai' ),
				HowToBlock::BLOCK_NAME,
				$mapped['attributes'],
				$mapped['preview']
			);
		}
		return null;
	}

	/**
	 * Prose FAQ heuristic (requires real Q/A pairs, not a title alone).
	 *
	 * @param string $content Content.
	 * @param string $title   Title.
	 * @return array|null
	 */
	private static function detect_prose_faq( $content, $title ) {
		unset( $title );
		$html  = self::normalize_html_fragment( $content );
		$pairs = self::extract_qa_pairs( $html );
		if ( count( $pairs ) < 2 ) {
			// Single pair only accepted under an explicit FAQ section heading.
			$section = self::find_faq_section_heading( $html );
			if ( '' === $section || count( $pairs ) < 1 ) {
				return null;
			}
		}
		if ( count( $pairs ) < 1 ) {
			return null;
		}

		$heading = self::find_faq_section_heading( $html );
		$items   = array();
		foreach ( array_slice( $pairs, 0, 20 ) as $pair ) {
			$items[] = array(
				'question' => $pair['question'],
				'answer'   => $pair['answer'],
			);
		}
		$sanitized = FaqBlockAttributeSanitizer::sanitize(
			array(
				'heading' => $heading,
				'items'   => $items,
			)
		);
		$complete = FaqBlockAttributeSanitizer::get_complete_items( $sanitized['attributes'] );
		if ( count( $complete ) < 1 ) {
			return null;
		}
		$confidence = count( $complete ) >= 2 ? self::CONFIDENCE_MEDIUM : self::CONFIDENCE_MEDIUM;
		$reason     = __( 'Detected question-and-answer pairs in the content. Confirm to create an FAQ block — nothing is published until you confirm.', 'seovault-ai' );
		if ( '' !== $heading ) {
			$reason = __( 'Detected a FAQ-style section with question-and-answer pairs. Confirm to create an FAQ block.', 'seovault-ai' );
		}

		return self::build_suggestion(
			self::KIND_FAQ,
			self::SOURCE_PROSE,
			$confidence,
			$reason,
			FaqBlock::BLOCK_NAME,
			self::client_safe_faq_attributes( $sanitized['attributes'] ),
			self::faq_preview_rows( $sanitized['attributes'] )
		);
	}

	/**
	 * Prose HowTo heuristic (ordered steps under how-to context).
	 *
	 * @param string $content Content.
	 * @param string $title   Title.
	 * @return array|null
	 */
	private static function detect_prose_howto( $content, $title ) {
		$html   = self::normalize_html_fragment( $content );
		$heading = self::find_howto_section_heading( $html );
		$steps   = self::extract_howto_steps( $html, $heading );
		if ( count( $steps ) < 2 ) {
			return null;
		}
		// Reject shopping / tag-style lists (very short items, no verbs).
		$substantive = 0;
		foreach ( $steps as $step ) {
			$text = trim( wp_strip_all_tags( (string) ( $step['text'] ?? '' ) ) );
			$name = trim( (string) ( $step['name'] ?? '' ) );
			$blob = trim( $name . ' ' . $text );
			if ( self::is_substantive_step( $blob ) ) {
				++$substantive;
			}
		}
		if ( $substantive < 2 ) {
			return null;
		}

		$name = $heading;
		if ( '' === $name && self::looks_like_howto_title( $title ) ) {
			// Title may label the how-to, but body steps are still required (already checked).
			$name = trim( $title );
		}
		if ( '' === $name ) {
			$name = __( 'How to complete this task', 'seovault-ai' );
		}

		$sanitized = HowToBlockAttributeSanitizer::sanitize(
			array(
				'name'  => $name,
				'steps' => $steps,
			)
		);
		$complete = HowToBlockAttributeSanitizer::get_complete_steps( $sanitized['attributes'] );
		if ( count( $complete ) < 2 ) {
			return null;
		}

		return self::build_suggestion(
			self::KIND_HOWTO,
			self::SOURCE_PROSE,
			self::CONFIDENCE_MEDIUM,
			__( 'Detected ordered procedural steps. Confirm to create a How-to block — schema is not emitted until the block exists.', 'seovault-ai' ),
			HowToBlock::BLOCK_NAME,
			self::client_safe_howto_attributes( $sanitized['attributes'] ),
			self::howto_preview_rows( $sanitized['attributes'] )
		);
	}

	/**
	 * Map Ultra HowTo rich config to Gutenberg attributes.
	 *
	 * @param mixed $raw Raw config.
	 * @return array{ok:bool,attributes:array,preview:array}
	 */
	private static function map_howto_rich_config( $raw ) {
		if ( ! is_array( $raw ) ) {
			return array( 'ok' => false, 'attributes' => array(), 'preview' => array() );
		}
		$mapped = array(
			'schemaVersion' => HowToBlockAttributeSanitizer::SCHEMA_VERSION,
			'name'          => isset( $raw['name'] ) ? $raw['name'] : ( isset( $raw['title'] ) ? $raw['title'] : '' ),
			'description'   => isset( $raw['description'] ) ? $raw['description'] : '',
			'steps'         => isset( $raw['steps'] ) ? $raw['steps'] : array(),
			'supplies'      => isset( $raw['supplies'] ) ? $raw['supplies'] : array(),
			'tools'         => isset( $raw['tools'] ) ? $raw['tools'] : array(),
			'totalTime'     => isset( $raw['totalTime'] ) ? $raw['totalTime'] : '',
			'estimatedCost' => isset( $raw['estimatedCost'] ) ? $raw['estimatedCost'] : array(),
			'displayMode'   => isset( $raw['displayMode'] ) ? $raw['displayMode'] : 'numbered',
			'stylePreset'   => isset( $raw['stylePreset'] ) ? $raw['stylePreset'] : 'clean',
			'colorPreset'   => isset( $raw['colorPreset'] ) ? $raw['colorPreset'] : 'blue',
			'density'       => isset( $raw['density'] ) ? $raw['density'] : 'comfortable',
		);
		$sanitized = HowToBlockAttributeSanitizer::sanitize( $mapped );
		$complete  = HowToBlockAttributeSanitizer::get_complete_steps( $sanitized['attributes'] );
		$name      = isset( $sanitized['attributes']['name'] ) ? trim( (string) $sanitized['attributes']['name'] ) : '';
		if ( '' === $name || count( $complete ) < 1 ) {
			return array( 'ok' => false, 'attributes' => array(), 'preview' => array() );
		}
		return array(
			'ok'         => true,
			'attributes' => self::client_safe_howto_attributes( $sanitized['attributes'] ),
			'preview'    => self::howto_preview_rows( $sanitized['attributes'] ),
		);
	}

	/**
	 * Extract data-seovault-config JSON for a rich-block type.
	 *
	 * @param string $content Content.
	 * @param string $type    Rich block type attribute.
	 * @return array<int,mixed>
	 */
	private static function extract_rich_block_configs( $content, $type ) {
		$out = array();
		if ( ! is_string( $content ) || '' === $content ) {
			return $out;
		}
		$previous = libxml_use_internal_errors( true );
		$dom      = new \DOMDocument();
		$wrapped  = '<?xml encoding="utf-8" ?><div id="seovault-scan">' . $content . '</div>';
		$loaded   = @$dom->loadHTML( $wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
		libxml_clear_errors();
		libxml_use_internal_errors( $previous );
		if ( ! $loaded ) {
			return $out;
		}
		$xpath = new \DOMXPath( $dom );
		$query = '//*[@data-seovault-rich-block="' . $type . '"]';
		foreach ( $xpath->query( $query ) as $node ) {
			if ( ! $node instanceof \DOMElement ) {
				continue;
			}
			$raw = $node->getAttribute( 'data-seovault-config' );
			$parsed = LegacyFaqRichBlockConverter::parse_config_attribute( $raw );
			if ( isset( $parsed['decoded'] ) ) {
				$out[] = $parsed['decoded'];
			}
		}
		return $out;
	}

	/**
	 * Extract Q/A pairs from HTML.
	 *
	 * @param string $html HTML fragment.
	 * @return array<int,array{question:string,answer:string}>
	 */
	private static function extract_qa_pairs( $html ) {
		$pairs = array();
		$previous = libxml_use_internal_errors( true );
		$dom      = new \DOMDocument();
		$wrapped  = '<?xml encoding="utf-8" ?><div id="seovault-scan">' . $html . '</div>';
		@$dom->loadHTML( $wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
		libxml_clear_errors();
		libxml_use_internal_errors( $previous );

		$xpath = new \DOMXPath( $dom );

		// Definition lists.
		foreach ( $xpath->query( '//dt' ) as $dt ) {
			if ( ! $dt instanceof \DOMElement ) {
				continue;
			}
			$question = trim( wp_strip_all_tags( $dt->textContent ) );
			$dd       = $dt->nextSibling;
			while ( $dd && ( ! $dd instanceof \DOMElement || 'dd' !== strtolower( $dd->nodeName ) ) ) {
				$dd = $dd->nextSibling;
			}
			if ( ! $dd instanceof \DOMElement ) {
				continue;
			}
			$answer = trim( wp_strip_all_tags( $dd->textContent ) );
			if ( self::is_valid_qa( $question, $answer ) ) {
				$pairs[] = array( 'question' => $question, 'answer' => $answer );
			}
		}

		// Headings that are questions, followed by paragraphs.
		foreach ( $xpath->query( '//h2|//h3|//h4|//strong|//b|//p' ) as $node ) {
			if ( ! $node instanceof \DOMElement ) {
				continue;
			}
			$text = trim( wp_strip_all_tags( $node->textContent ) );
			if ( ! self::looks_like_question( $text ) ) {
				continue;
			}
			$answer = self::collect_following_answer( $node );
			if ( self::is_valid_qa( $text, $answer ) ) {
				$pairs[] = array( 'question' => $text, 'answer' => $answer );
			}
		}

		// Deduplicate by normalized question.
		$seen = array();
		$uniq = array();
		foreach ( $pairs as $pair ) {
			$key = strtolower( preg_replace( '/\s+/u', ' ', $pair['question'] ) );
			if ( isset( $seen[ $key ] ) ) {
				continue;
			}
			$seen[ $key ] = true;
			$uniq[]       = $pair;
		}
		return $uniq;
	}

	/**
	 * Collect answer text after a question node.
	 *
	 * @param \DOMElement $node Question node.
	 * @return string
	 */
	private static function collect_following_answer( \DOMElement $node ) {
		$parts = array();
		$sib   = $node->nextSibling;
		$count = 0;
		while ( $sib && $count < 4 ) {
			if ( $sib instanceof \DOMElement ) {
				$name = strtolower( $sib->nodeName );
				if ( in_array( $name, array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ), true ) ) {
					break;
				}
				$text = trim( wp_strip_all_tags( $sib->textContent ) );
				if ( '' !== $text ) {
					if ( self::looks_like_question( $text ) ) {
						break;
					}
					$parts[] = $text;
					++$count;
				}
			}
			$sib = $sib->nextSibling;
		}
		return trim( implode( "\n\n", $parts ) );
	}

	/**
	 * Extract HowTo steps from ordered lists / numbered paragraphs.
	 *
	 * @param string $html    HTML.
	 * @param string $heading Section heading if any.
	 * @return array<int,array{name:string,text:string}>
	 */
	private static function extract_howto_steps( $html, $heading ) {
		unset( $heading );
		$steps    = array();
		$previous = libxml_use_internal_errors( true );
		$dom      = new \DOMDocument();
		$wrapped  = '<?xml encoding="utf-8" ?><div id="seovault-scan">' . $html . '</div>';
		@$dom->loadHTML( $wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
		libxml_clear_errors();
		libxml_use_internal_errors( $previous );
		$xpath = new \DOMXPath( $dom );

		foreach ( $xpath->query( '//ol' ) as $ol ) {
			if ( ! $ol instanceof \DOMElement ) {
				continue;
			}
			$local = array();
			foreach ( $xpath->query( './li', $ol ) as $li ) {
				if ( ! $li instanceof \DOMElement ) {
					continue;
				}
				$text = trim( wp_strip_all_tags( $li->textContent ) );
				if ( '' === $text ) {
					continue;
				}
				$local[] = self::split_step_name_text( $text );
			}
			if ( count( $local ) >= 2 && count( $local ) > count( $steps ) ) {
				$steps = $local;
			}
		}

		if ( count( $steps ) >= 2 ) {
			return $steps;
		}

		// Numbered paragraphs: "1. …" / "2) …"
		$paras = array();
		foreach ( $xpath->query( '//p' ) as $p ) {
			if ( ! $p instanceof \DOMElement ) {
				continue;
			}
			$text = trim( wp_strip_all_tags( $p->textContent ) );
			if ( preg_match( '/^\d+[\.\)]\s+(.+)$/u', $text, $m ) ) {
				$paras[] = self::split_step_name_text( $m[1] );
			}
		}
		return count( $paras ) >= 2 ? $paras : array();
	}

	/**
	 * Split a step blob into short name + text.
	 *
	 * @param string $text Step text.
	 * @return array{name:string,text:string}
	 */
	private static function split_step_name_text( $text ) {
		$text = trim( (string) $text );
		if ( preg_match( '/^(.{3,80}?)[:.\-–—]\s+(.+)$/u', $text, $m ) ) {
			return array(
				'name' => trim( $m[1] ),
				'text' => trim( $m[2] ),
			);
		}
		if ( mb_strlen( $text ) <= 80 ) {
			return array(
				'name' => $text,
				'text' => $text,
			);
		}
		return array(
			'name' => trim( mb_substr( $text, 0, 60 ) ),
			'text' => $text,
		);
	}

	/**
	 * Find FAQ section heading text.
	 *
	 * @param string $html HTML.
	 * @return string
	 */
	private static function find_faq_section_heading( $html ) {
		$previous = libxml_use_internal_errors( true );
		$dom      = new \DOMDocument();
		@$dom->loadHTML( '<?xml encoding="utf-8" ?><div>' . $html . '</div>', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
		libxml_clear_errors();
		libxml_use_internal_errors( $previous );
		$xpath = new \DOMXPath( $dom );
		foreach ( $xpath->query( '//h2|//h3|//h4' ) as $node ) {
			$text = trim( wp_strip_all_tags( $node->textContent ) );
			if ( self::looks_like_faq_heading( $text ) ) {
				return $text;
			}
		}
		return '';
	}

	/**
	 * Find HowTo section heading.
	 *
	 * @param string $html HTML.
	 * @return string
	 */
	private static function find_howto_section_heading( $html ) {
		$previous = libxml_use_internal_errors( true );
		$dom      = new \DOMDocument();
		@$dom->loadHTML( '<?xml encoding="utf-8" ?><div>' . $html . '</div>', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); // phpcs:ignore
		libxml_clear_errors();
		libxml_use_internal_errors( $previous );
		$xpath = new \DOMXPath( $dom );
		foreach ( $xpath->query( '//h2|//h3|//h4' ) as $node ) {
			$text = trim( wp_strip_all_tags( $node->textContent ) );
			if ( self::looks_like_howto_heading( $text ) ) {
				return $text;
			}
		}
		return '';
	}

	/**
	 * @param string $text Text.
	 * @return bool
	 */
	public static function looks_like_faq_title( $text ) {
		$text = trim( (string) $text );
		if ( '' === $text ) {
			return false;
		}
		return (bool) preg_match( '/\b(faq|frequently\s+asked|questions?\s+and\s+answers?|q\s*&\s*a)\b/iu', $text );
	}

	/**
	 * @param string $text Text.
	 * @return bool
	 */
	public static function looks_like_faq_heading( $text ) {
		return self::looks_like_faq_title( $text );
	}

	/**
	 * @param string $text Text.
	 * @return bool
	 */
	public static function looks_like_howto_title( $text ) {
		$text = trim( (string) $text );
		if ( '' === $text ) {
			return false;
		}
		return (bool) preg_match( '/^\s*how\s+to\b/iu', $text )
			|| (bool) preg_match( '/\b(step[-\s]?by[-\s]?step|instructions?|tutorial)\b/iu', $text );
	}

	/**
	 * @param string $text Text.
	 * @return bool
	 */
	public static function looks_like_howto_heading( $text ) {
		return self::looks_like_howto_title( $text )
			|| (bool) preg_match( '/\b(steps?|procedure|directions)\b/iu', $text );
	}

	/**
	 * @param string $text Text.
	 * @return bool
	 */
	private static function looks_like_question( $text ) {
		$text = trim( (string) $text );
		if ( mb_strlen( $text ) < 8 || mb_strlen( $text ) > 200 ) {
			return false;
		}
		if ( '?' === mb_substr( $text, -1 ) ) {
			return true;
		}
		return (bool) preg_match( '/^(what|why|how|when|where|who|which|can|does|do|is|are|should)\b/iu', $text );
	}

	/**
	 * @param string $question Question.
	 * @param string $answer   Answer.
	 * @return bool
	 */
	private static function is_valid_qa( $question, $answer ) {
		$question = trim( (string) $question );
		$answer   = trim( (string) $answer );
		if ( mb_strlen( $question ) < 8 || mb_strlen( $answer ) < 12 ) {
			return false;
		}
		if ( self::looks_like_placeholder( $question ) || self::looks_like_placeholder( $answer ) ) {
			return false;
		}
		return true;
	}

	/**
	 * @param string $text Text.
	 * @return bool
	 */
	private static function looks_like_placeholder( $text ) {
		return (bool) preg_match( '/^(answer\s+goes\s+here|lorem\s+ipsum|todo|tbd|placeholder)\b/iu', trim( (string) $text ) );
	}

	/**
	 * @param string $text Step blob.
	 * @return bool
	 */
	private static function is_substantive_step( $text ) {
		$text = trim( (string) $text );
		if ( mb_strlen( $text ) < 12 ) {
			return false;
		}
		// Shopping-list style: 1–3 words, no verb-ish tokens.
		$words = preg_split( '/\s+/u', $text );
		if ( is_array( $words ) && count( $words ) <= 3 && ! preg_match( '/\b(add|mix|cut|press|open|wait|apply|remove|check|place|insert|connect|install|create|write|save|click|select|enter|fill|run|start|stop|repeat|follow|use|take|put|set|make|ensure|verify)\b/iu', $text ) ) {
			return false;
		}
		return true;
	}

	/**
	 * @param string $content Content.
	 * @return bool
	 */
	private static function is_effectively_empty_body( $content ) {
		$plain = trim( wp_strip_all_tags( (string) $content ) );
		$plain = preg_replace( '/\s+/u', '', $plain );
		return '' === $plain || mb_strlen( (string) $plain ) < 40;
	}

	/**
	 * @param string $content Gutenberg/HTML content.
	 * @return string
	 */
	private static function normalize_html_fragment( $content ) {
		$content = (string) $content;
		// Prefer rendered block HTML when classic markup is missing.
		if ( false !== strpos( $content, '<!-- wp:' ) && function_exists( 'do_blocks' ) ) {
			// Avoid full do_blocks side effects; strip block comments lightly.
			$content = preg_replace( '/<!--\s*\/?wp:[^>]+-->/', '', $content );
		}
		return $content;
	}

	/**
	 * @param array  $blocks Block tree.
	 * @param string $name   Block name.
	 * @return bool
	 */
	private static function blocks_contain( array $blocks, $name ) {
		foreach ( $blocks as $block ) {
			if ( ! is_array( $block ) ) {
				continue;
			}
			if ( isset( $block['blockName'] ) && $name === $block['blockName'] ) {
				return true;
			}
			if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) && self::blocks_contain( $block['innerBlocks'], $name ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * @param string $kind       Kind.
	 * @param string $source     Source.
	 * @param string $confidence Confidence.
	 * @param string $reason     Reason.
	 * @param string $block_name Block name.
	 * @param array  $attributes Attributes.
	 * @param array  $preview    Preview rows.
	 * @return array
	 */
	private static function build_suggestion( $kind, $source, $confidence, $reason, $block_name, array $attributes, array $preview ) {
		$id = $kind . ':' . substr( hash( 'sha256', (string) wp_json_encode( array( $source, $attributes ) ) ), 0, 12 );
		return array(
			'id'          => $id,
			'kind'        => $kind,
			'type_label'  => self::KIND_HOWTO === $kind ? 'HowTo' : 'FAQPage',
			'block_name'  => $block_name,
			'source'      => $source,
			'confidence'  => $confidence,
			'reason'      => $reason,
			'summary'     => self::KIND_HOWTO === $kind
				? __( 'Suggested How-to block', 'seovault-ai' )
				: __( 'Suggested FAQ block', 'seovault-ai' ),
			'attributes'  => $attributes,
			'preview'     => $preview,
			'item_count'  => count( $preview ),
		);
	}

	/**
	 * @param array $attributes FAQ attributes.
	 * @return array
	 */
	private static function client_safe_faq_attributes( array $attributes ) {
		$items = array();
		foreach ( isset( $attributes['items'] ) && is_array( $attributes['items'] ) ? $attributes['items'] : array() as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			$items[] = array(
				'id'       => (string) ( $item['id'] ?? '' ),
				'question' => (string) ( $item['question'] ?? '' ),
				'answer'   => (string) ( $item['answer'] ?? '' ),
			);
		}
		return array(
			'schemaVersion'       => FaqBlockAttributeSanitizer::SCHEMA_VERSION,
			'heading'             => (string) ( $attributes['heading'] ?? '' ),
			'intro'               => (string) ( $attributes['intro'] ?? '' ),
			'items'               => $items,
			'displayMode'         => (string) ( $attributes['displayMode'] ?? 'accordion' ),
			'stylePreset'         => (string) ( $attributes['stylePreset'] ?? 'accordion-cards' ),
			'openFirstItem'       => ! empty( $attributes['openFirstItem'] ),
			'showQuestionNumbers' => ! empty( $attributes['showQuestionNumbers'] ),
			'colorPreset'         => (string) ( $attributes['colorPreset'] ?? 'blue' ),
			'density'             => (string) ( $attributes['density'] ?? 'comfortable' ),
			'sharpCorners'        => ! empty( $attributes['sharpCorners'] ),
		);
	}

	/**
	 * @param array $attributes HowTo attributes.
	 * @return array
	 */
	private static function client_safe_howto_attributes( array $attributes ) {
		$steps = array();
		foreach ( isset( $attributes['steps'] ) && is_array( $attributes['steps'] ) ? $attributes['steps'] : array() as $step ) {
			if ( ! is_array( $step ) ) {
				continue;
			}
			$steps[] = array(
				'id'      => (string) ( $step['id'] ?? '' ),
				'name'    => (string) ( $step['name'] ?? '' ),
				'text'    => (string) ( $step['text'] ?? '' ),
				'imageId' => isset( $step['imageId'] ) ? (int) $step['imageId'] : 0,
			);
		}
		return array(
			'schemaVersion' => HowToBlockAttributeSanitizer::SCHEMA_VERSION,
			'name'          => (string) ( $attributes['name'] ?? '' ),
			'description'   => (string) ( $attributes['description'] ?? '' ),
			'steps'         => $steps,
			'supplies'      => isset( $attributes['supplies'] ) && is_array( $attributes['supplies'] ) ? array_values( $attributes['supplies'] ) : array(),
			'tools'         => isset( $attributes['tools'] ) && is_array( $attributes['tools'] ) ? array_values( $attributes['tools'] ) : array(),
			'totalTime'     => (string) ( $attributes['totalTime'] ?? '' ),
			'estimatedCost' => isset( $attributes['estimatedCost'] ) && is_array( $attributes['estimatedCost'] ) ? $attributes['estimatedCost'] : array( 'currency' => '', 'value' => '' ),
			'displayMode'   => (string) ( $attributes['displayMode'] ?? 'numbered' ),
			'stylePreset'   => (string) ( $attributes['stylePreset'] ?? 'clean' ),
			'colorPreset'   => (string) ( $attributes['colorPreset'] ?? 'blue' ),
			'density'       => (string) ( $attributes['density'] ?? 'comfortable' ),
		);
	}

	/**
	 * @param array $attributes FAQ attributes.
	 * @return array<int,array{label:string,detail:string}>
	 */
	private static function faq_preview_rows( array $attributes ) {
		$rows = array();
		foreach ( FaqBlockAttributeSanitizer::get_complete_items( $attributes ) as $item ) {
			$rows[] = array(
				'label'  => (string) ( $item['question'] ?? '' ),
				'detail' => (string) ( $item['answer_plain'] ?? wp_strip_all_tags( (string) ( $item['answer'] ?? '' ) ) ),
			);
		}
		return $rows;
	}

	/**
	 * @param array $attributes HowTo attributes.
	 * @return array<int,array{label:string,detail:string}>
	 */
	private static function howto_preview_rows( array $attributes ) {
		$rows = array();
		foreach ( HowToBlockAttributeSanitizer::get_complete_steps( $attributes ) as $step ) {
			$rows[] = array(
				'label'  => (string) ( $step['name'] ?? '' ),
				'detail' => (string) ( $step['text_plain'] ?? wp_strip_all_tags( (string) ( $step['text'] ?? '' ) ) ),
			);
		}
		return $rows;
	}
}
