<?php
/**
 * Collect SEOvault FAQ blocks from saved post content for FAQPage emission.
 *
 * Parses blocks before wp_head (via SchemaManager::build_result). Sanitizes
 * attributes server-side, keeps only complete visible Q/A pairs, combines
 * multiple blocks into one FAQPage, and deduplicates by normalized question.
 *
 * @package SEOvaultSEO\Blocks
 */

namespace SEOvaultSEO\Blocks;

use SEOvaultSEO\Schema\SchemaSourceDescriptor;

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

/**
 * Builds a single block-derived FAQPage candidate from post content.
 */
class FaqBlockCollector {

	const SOURCE       = 'block';
	const SOURCE_REF   = 'block:seovault/faq';
	const BLOCK_NAME   = 'seovault/faq';
	const MAX_REUSABLE = 10;

	/**
	 * Collect FAQ block data from a post.
	 *
	 * @param \WP_Post|int $post Post or ID.
	 * @return array{
	 *   node: array|null,
	 *   pairs: array<int,array>,
	 *   blocks: array<int,array>,
	 *   withheld: array<int,array>,
	 *   improvements: array<int,array>,
	 *   source_ref: string,
	 *   content_fingerprint: string,
	 *   setup: array<string,mixed>
	 * }
	 */
	public static function collect_from_post( $post ) {
		$empty = self::empty_result();

		if ( is_numeric( $post ) ) {
			$post = get_post( (int) $post );
		}
		if ( ! $post instanceof \WP_Post ) {
			return $empty;
		}

		$content = (string) $post->post_content;
		$blocks  = \SEOvaultSEO\Schema\SchemaBlockParseCache::parse( $content );
		$counter = 0;
		$visited = array();
		$found   = self::walk_blocks( $blocks, $counter, $visited );

		$pairs            = array();
		$seen_questions   = array();
		$withheld         = array();
		$improvements     = array();
		$block_summaries  = array();
		$complete_blocks  = 0;
		$incomplete_only  = 0;
		$duplicate_skips  = 0;
		$heading          = '';

		foreach ( $found as $entry ) {
			$attrs      = isset( $entry['attributes'] ) && is_array( $entry['attributes'] ) ? $entry['attributes'] : array();
			$sanitized  = FaqBlockAttributeSanitizer::sanitize( $attrs );
			$clean      = $sanitized['attributes'];
			$complete   = FaqBlockAttributeSanitizer::get_complete_items( $clean );
			$all_items  = isset( $clean['items'] ) && is_array( $clean['items'] ) ? $clean['items'] : array();
			$block_ref  = self::SOURCE_REF . ':' . (int) $entry['index'];
			$has_partial = self::has_partial_pairs( $all_items );

			$block_summaries[] = array(
				'index'           => (int) $entry['index'],
				'source_ref'      => $block_ref,
				'heading'         => (string) ( $clean['heading'] ?? '' ),
				'complete_count'  => count( $complete ),
				'item_count'      => count( $all_items ),
				'from_reusable'   => ! empty( $entry['from_reusable'] ),
			);

			if ( '' === $heading && ! empty( $clean['heading'] ) ) {
				$heading = (string) $clean['heading'];
			}

			if ( empty( $complete ) ) {
				if ( ! empty( $all_items ) || $has_partial || '' !== trim( (string) ( $clean['heading'] ?? '' ) ) || '' !== trim( (string) ( $clean['intro'] ?? '' ) ) ) {
					++$incomplete_only;
					$withheld[] = self::withheld_incomplete_block( $block_ref, $clean, $entry['index'] );
				}
				continue;
			}

			++$complete_blocks;
			if ( $has_partial ) {
				$improvements[] = array(
					'code'       => 'faq.block.partial_rows',
					'type'       => 'FAQPage',
					'source'     => self::SOURCE,
					'source_ref' => $block_ref,
					'field_key'  => 'mainEntity',
					'action_key' => 'review_partial_faq_rows',
					'kind'       => 'improvement',
					'message'    => 'Incomplete FAQ rows were excluded from schema.',
				);
			}

			foreach ( $complete as $item ) {
				$question = (string) ( $item['question'] ?? '' );
				$norm     = FaqBlockAttributeSanitizer::normalize_question_for_schema( $question );
				if ( '' === $norm ) {
					continue;
				}
				if ( isset( $seen_questions[ $norm ] ) ) {
					++$duplicate_skips;
					$improvements[] = array(
						'code'       => 'faq.block.duplicate_question',
						'type'       => 'FAQPage',
						'source'     => self::SOURCE,
						'source_ref' => $block_ref,
						'field_key'  => 'mainEntity',
						'action_key' => 'remove_duplicate_faq_question',
						'kind'       => 'improvement',
						'message'    => 'Duplicate FAQ question skipped (normalized match).',
						'question'   => $question,
					);
					continue;
				}
				$seen_questions[ $norm ] = true;
				$pairs[]                 = array(
					'id'           => (string) ( $item['id'] ?? '' ),
					'question'     => $question,
					'answer'       => (string) ( $item['answer'] ?? '' ),
					'answer_plain' => (string) ( $item['answer_plain'] ?? FaqBlockAttributeSanitizer::normalize_plain_answer( $item['answer'] ?? '' ) ),
					'source_ref'   => $block_ref,
					'block_index'  => (int) $entry['index'],
				);
			}
		}

		$fingerprint = self::fingerprint_content( $content, $pairs );

		// Legacy web FAQ HTML is never trusted for emission; surface conversion only.
		foreach ( LegacyFaqRichBlockConverter::improvement_descriptors( $content ) as $legacy_improvement ) {
			$improvements[] = array_merge(
				array(
					'type'       => 'FAQPage',
					'source'     => 'legacy_rich_block',
					'source_ref' => 'legacy:faq-schema',
					'field_key'  => 'mainEntity',
					'action_key' => 'convert_legacy_faq_rich_block',
					'kind'       => 'improvement',
				),
				$legacy_improvement
			);
		}

		foreach ( ThirdPartyBlockConverter::improvement_descriptors( $content ) as $third_party ) {
			$improvements[] = array_merge(
				array(
					'type'       => 'FAQPage',
					'source'     => 'third_party_block',
					'source_ref' => 'thirdparty:faq-howto',
					'field_key'  => 'mainEntity',
					'action_key' => 'convert_thirdparty_blocks',
					'kind'       => 'improvement',
				),
				$third_party
			);
		}

		if ( empty( $pairs ) ) {
			$result                      = self::empty_result();
			$result['withheld']          = $withheld;
			$result['improvements']      = $improvements;
			$result['blocks']            = $block_summaries;
			$result['content_fingerprint'] = $fingerprint;
			$result['setup']             = array(
				'status'            => empty( $found ) ? 'absent' : 'incomplete',
				'block_count'       => count( $found ),
				'complete_blocks'   => 0,
				'incomplete_blocks' => $incomplete_only,
				'pair_count'        => 0,
				'duplicates_skipped'=> $duplicate_skips,
			);
			return $result;
		}

		$node = self::build_faq_page_node( $post, $pairs, $heading );

		return array(
			'node'                 => $node,
			'pairs'                => $pairs,
			'blocks'               => $block_summaries,
			'withheld'             => $withheld,
			'improvements'         => $improvements,
			'source_ref'           => self::SOURCE_REF,
			'content_fingerprint'  => $fingerprint,
			'setup'                => array(
				'status'             => 'ready',
				'block_count'        => count( $found ),
				'complete_blocks'    => $complete_blocks,
				'incomplete_blocks'  => $incomplete_only,
				'pair_count'         => count( $pairs ),
				'duplicates_skipped' => $duplicate_skips,
			),
		);
	}

	/**
	 * Whether the post content contains any seovault/faq block (complete or not).
	 *
	 * @param \WP_Post|int $post Post.
	 * @return bool
	 */
	public static function post_has_faq_block( $post ) {
		$result = self::collect_from_post( $post );
		return ! empty( $result['blocks'] );
	}

	/**
	 * Walk parsed blocks (including nested and reusable) for FAQ blocks.
	 *
	 * @param array $blocks         Parsed blocks.
	 * @param int   $counter        Running index (by ref).
	 * @param array $visited_refs   Reusable post IDs already expanded.
	 * @param bool  $from_reusable  Whether currently inside a reusable expansion.
	 * @return array<int,array{index:int,attributes:array,from_reusable:bool}>
	 */
	private static function walk_blocks( array $blocks, &$counter, array &$visited_refs, $from_reusable = false ) {
		$found = array();

		foreach ( $blocks as $block ) {
			if ( ! is_array( $block ) ) {
				continue;
			}

			$name = isset( $block['blockName'] ) ? (string) $block['blockName'] : '';

			if ( self::BLOCK_NAME === $name ) {
				$found[] = array(
					'index'         => (int) $counter,
					'attributes'    => isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array(),
					'from_reusable' => (bool) $from_reusable,
				);
				++$counter;
			}

			if ( 'core/block' === $name ) {
				$ref = isset( $block['attrs']['ref'] ) ? (int) $block['attrs']['ref'] : 0;
				if ( $ref > 0 && ! isset( $visited_refs[ $ref ] ) && count( $visited_refs ) < self::MAX_REUSABLE ) {
					$visited_refs[ $ref ] = true;
					$reusable             = get_post( $ref );
					if ( $reusable instanceof \WP_Post && 'publish' === $reusable->post_status ) {
						$inner  = function_exists( 'parse_blocks' ) ? parse_blocks( (string) $reusable->post_content ) : array();
						$nested = self::walk_blocks( $inner, $counter, $visited_refs, true );
						foreach ( $nested as $row ) {
							$found[] = $row;
						}
					}
				}
			}

			if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
				$nested = self::walk_blocks( $block['innerBlocks'], $counter, $visited_refs, $from_reusable );
				foreach ( $nested as $row ) {
					$found[] = $row;
				}
			}
		}

		return $found;
	}

	/**
	 * Build the FAQPage graph node.
	 *
	 * @param \WP_Post $post    Post.
	 * @param array    $pairs   Deduped complete pairs.
	 * @param string   $heading Optional heading from first block.
	 * @return array
	 */
	private static function build_faq_page_node( \WP_Post $post, array $pairs, $heading ) {
		$permalink = get_permalink( $post );
		if ( ! is_string( $permalink ) || '' === $permalink ) {
			$permalink = home_url( '/?p=' . (int) $post->ID );
		}
		$permalink = trailingslashit( $permalink );
		// Prefer non-trailing for fragment ids when permalink has no trailing slash conventionally.
		$permalink = untrailingslashit( $permalink );

		$faq_id = $permalink . '#faq';
		$name   = '' !== trim( (string) $heading ) ? (string) $heading : (string) get_the_title( $post );

		$main_entity = array();
		foreach ( $pairs as $pair ) {
			$item_id   = (string) ( $pair['id'] ?? '' );
			$question  = (string) ( $pair['question'] ?? '' );
			$plain     = (string) ( $pair['answer_plain'] ?? '' );
			$entity_id = '' !== $item_id
				? $permalink . '#faq-question-' . sanitize_title( $item_id )
				: $permalink . '#faq-question-' . substr( hash( 'sha256', SchemaSourceDescriptor::normalize_faq_question( $question ) ), 0, 12 );

			$main_entity[] = array(
				'@type'          => 'Question',
				'@id'            => $entity_id,
				'name'           => $question,
				'acceptedAnswer' => array(
					'@type' => 'Answer',
					'text'  => $plain,
				),
			);
		}

		$node = array(
			'@type'      => 'FAQPage',
			'@id'        => $faq_id,
			'mainEntity' => $main_entity,
		);

		if ( '' !== trim( $name ) ) {
			$node['name'] = $name;
		}

		/**
		 * Filter the block-derived FAQPage node before emission.
		 *
		 * @param array    $node  FAQPage node.
		 * @param \WP_Post $post  Post.
		 * @param array    $pairs Complete pairs.
		 */
		$filtered = apply_filters( 'seovaultseo_faq_block_schema_node', $node, $post, $pairs );
		return is_array( $filtered ) ? $filtered : $node;
	}

	/**
	 * @param array $items Sanitized items.
	 * @return bool
	 */
	private static function has_partial_pairs( array $items ) {
		foreach ( $items as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			$question = isset( $item['question'] ) ? trim( (string) $item['question'] ) : '';
			$plain    = isset( $item['answer_plain'] )
				? (string) $item['answer_plain']
				: FaqBlockAttributeSanitizer::normalize_plain_answer( $item['answer'] ?? '' );
			$has_q    = '' !== $question;
			$has_a    = '' !== trim( $plain );
			if ( ( $has_q && ! $has_a ) || ( ! $has_q && $has_a ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * @param string $source_ref Source ref.
	 * @param array  $clean      Sanitized attrs.
	 * @param int    $index      Block index.
	 * @return array
	 */
	private static function withheld_incomplete_block( $source_ref, array $clean, $index ) {
		$setup = array(
			array(
				'code'       => 'readiness.faq.incomplete_pair',
				'kind'       => 'blocker',
				'field_key'  => 'mainEntity',
				'action_key' => 'add_complete_faq_pair',
			),
		);

		return array(
			'type'         => 'FAQPage',
			'@id'          => '',
			'source'       => self::SOURCE,
			'source_ref'   => $source_ref,
			'emitted'      => false,
			'withheld'     => true,
			'reason'       => 'faq_block_incomplete',
			'graph_index'  => -1,
			'block_index'  => (int) $index,
			'heading'      => (string) ( $clean['heading'] ?? '' ),
			'setup'        => $setup,
			'readiness'    => array(
				'ready'    => false,
				'blockers' => array(
					array(
						'code'       => 'readiness.faq.incomplete_pair',
						'field_key'  => 'mainEntity',
						'action_key' => 'add_complete_faq_pair',
					),
				),
			),
		);
	}

	/**
	 * @param string $content Post content.
	 * @param array  $pairs   Pairs.
	 * @return string
	 */
	private static function fingerprint_content( $content, array $pairs ) {
		$payload = array(
			'content_sha' => hash( 'sha256', (string) $content ),
			'pairs'       => array_map(
				static function ( $pair ) {
					return array(
						'q' => SchemaSourceDescriptor::normalize_faq_question( $pair['question'] ?? '' ),
						'a' => isset( $pair['answer_plain'] ) ? (string) $pair['answer_plain'] : '',
					);
				},
				$pairs
			),
		);
		$encoded = wp_json_encode( $payload );
		return 'sha256:' . hash( 'sha256', is_string( $encoded ) ? $encoded : serialize( $payload ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Hash input only.
	}

	/**
	 * @return array
	 */
	private static function empty_result() {
		return array(
			'node'                 => null,
			'pairs'                => array(),
			'blocks'               => array(),
			'withheld'             => array(),
			'improvements'         => array(),
			'source_ref'           => self::SOURCE_REF,
			'content_fingerprint'  => 'sha256:' . hash( 'sha256', '' ),
			'setup'                => array(
				'status'             => 'absent',
				'block_count'        => 0,
				'complete_blocks'    => 0,
				'incomplete_blocks'  => 0,
				'pair_count'         => 0,
				'duplicates_skipped' => 0,
			),
		);
	}
}
