<?php
/**
 * Collect SEOvault HowTo blocks from saved post content for HowTo emission.
 *
 * Parses blocks before wp_head (via SchemaManager::build_result). Sanitizes
 * attributes server-side, emits one HowTo node per ready block with a unique
 * @id (never type-only collapsing), and withholds incomplete blocks.
 *
 * @package SEOvaultSEO\Blocks
 */

namespace SEOvaultSEO\Blocks;

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

/**
 * Builds block-derived HowTo candidates from post content.
 */
class HowToBlockCollector {

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

	/**
	 * Collect HowTo block data from a post.
	 *
	 * @param \WP_Post|int $post Post or ID.
	 * @return array{
	 *   nodes: array<int,array>,
	 *   instances: 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 );

		$nodes           = array();
		$instances       = array();
		$withheld        = array();
		$improvements    = array();
		$block_summaries = array();
		$complete_blocks = 0;
		$incomplete_only = 0;
		$emit_index      = 0;

		foreach ( $found as $entry ) {
			$attrs     = isset( $entry['attributes'] ) && is_array( $entry['attributes'] ) ? $entry['attributes'] : array();
			$sanitized = HowToBlockAttributeSanitizer::sanitize( $attrs );
			$clean     = $sanitized['attributes'];
			$complete  = HowToBlockAttributeSanitizer::get_complete_steps( $clean );
			$all_steps = isset( $clean['steps'] ) && is_array( $clean['steps'] ) ? $clean['steps'] : array();
			$block_ref = self::SOURCE_REF . ':' . (int) $entry['index'];
			$name      = isset( $clean['name'] ) ? trim( (string) $clean['name'] ) : '';
			$ready     = HowToBlockAttributeSanitizer::evaluate_readiness( $clean )->is_ready();

			$block_summaries[] = array(
				'index'          => (int) $entry['index'],
				'source_ref'     => $block_ref,
				'name'           => $name,
				'complete_count' => count( $complete ),
				'step_count'     => count( $all_steps ),
				'from_reusable'  => ! empty( $entry['from_reusable'] ),
				'ready'          => $ready,
			);

			if ( ! $ready ) {
				if ( self::block_has_any_content( $clean ) ) {
					++$incomplete_only;
					$withheld[] = self::withheld_incomplete_block( $block_ref, $clean, $entry['index'] );
					if ( self::has_partial_steps( $all_steps ) ) {
						$improvements[] = array(
							'code'       => 'howto.block.partial_steps',
							'type'       => 'HowTo',
							'source'     => self::SOURCE,
							'source_ref' => $block_ref,
							'field_key'  => 'step',
							'action_key' => 'review_partial_howto_steps',
							'kind'       => 'improvement',
							'message'    => 'Incomplete HowTo steps were excluded from schema.',
						);
					}
				}
				continue;
			}

			++$complete_blocks;
			++$emit_index;

			if ( self::has_partial_steps( $all_steps ) ) {
				$improvements[] = array(
					'code'       => 'howto.block.partial_steps',
					'type'       => 'HowTo',
					'source'     => self::SOURCE,
					'source_ref' => $block_ref,
					'field_key'  => 'step',
					'action_key' => 'review_partial_howto_steps',
					'kind'       => 'improvement',
					'message'    => 'Incomplete HowTo steps were excluded from schema.',
				);
			}

			$node = self::build_howto_node( $post, $clean, $complete, $emit_index, $block_ref );
			$nodes[] = $node;
			$instances[] = array(
				'index'      => (int) $entry['index'],
				'source_ref' => $block_ref,
				'@id'        => (string) ( $node['@id'] ?? '' ),
				'name'       => $name,
				'steps'      => $complete,
			);
		}

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

		if ( empty( $nodes ) ) {
			$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,
				'instance_count'    => 0,
			);
			return $result;
		}

		return array(
			'nodes'                => $nodes,
			'instances'            => $instances,
			'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,
				'instance_count'     => count( $nodes ),
			),
		);
	}

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

	/**
	 * Walk parsed blocks (including nested and reusable) for HowTo 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 one HowTo graph node for a ready block.
	 *
	 * @param \WP_Post $post       Post.
	 * @param array    $clean      Sanitized attributes.
	 * @param array    $steps      Complete visible steps.
	 * @param int      $emit_index 1-based emission ordinal among ready blocks.
	 * @param string   $block_ref  Source ref.
	 * @return array
	 */
	private static function build_howto_node( \WP_Post $post, array $clean, array $steps, $emit_index, $block_ref ) {
		$permalink = get_permalink( $post );
		if ( ! is_string( $permalink ) || '' === $permalink ) {
			$permalink = home_url( '/?p=' . (int) $post->ID );
		}
		$permalink = untrailingslashit( $permalink );

		$fragment = 1 === (int) $emit_index ? 'howto' : 'howto-' . (int) $emit_index;
		$howto_id = $permalink . '#' . $fragment;

		$step_nodes = array();
		foreach ( $steps as $step ) {
			$step_id   = (string) ( $step['id'] ?? '' );
			$step_name = (string) ( $step['name'] ?? '' );
			$plain     = (string) ( $step['text_plain'] ?? HowToBlockAttributeSanitizer::normalize_plain_text( $step['text'] ?? '' ) );
			$entity_id = '' !== $step_id
				? $permalink . '#howto-step-' . sanitize_title( $step_id )
				: $permalink . '#howto-step-' . substr( hash( 'sha256', $step_name . '|' . $plain ), 0, 12 );

			$entry = array(
				'@type' => 'HowToStep',
				'@id'   => $entity_id,
			);
			if ( '' !== $step_name ) {
				$entry['name'] = $step_name;
			}
			if ( '' !== $plain ) {
				$entry['text'] = $plain;
			}
			$step_nodes[] = $entry;
		}

		$node = array(
			'@type' => 'HowTo',
			'@id'   => $howto_id,
			'name'  => (string) ( $clean['name'] ?? '' ),
			'step'  => $step_nodes,
		);

		$description = isset( $clean['description'] ) ? trim( (string) $clean['description'] ) : '';
		if ( '' !== $description ) {
			$node['description'] = $description;
		}

		$total_time = isset( $clean['totalTime'] ) ? trim( (string) $clean['totalTime'] ) : '';
		if ( '' !== $total_time ) {
			$node['totalTime'] = $total_time;
		}

		$cost = isset( $clean['estimatedCost'] ) && is_array( $clean['estimatedCost'] ) ? $clean['estimatedCost'] : array();
		$currency = isset( $cost['currency'] ) ? trim( (string) $cost['currency'] ) : '';
		$value    = isset( $cost['value'] ) ? trim( (string) $cost['value'] ) : '';
		if ( '' !== $currency && '' !== $value ) {
			$node['estimatedCost'] = array(
				'@type'    => 'MonetaryAmount',
				'currency' => $currency,
				'value'    => $value,
			);
		}

		$supplies = isset( $clean['supplies'] ) && is_array( $clean['supplies'] ) ? $clean['supplies'] : array();
		if ( ! empty( $supplies ) ) {
			$node['supply'] = array_map(
				static function ( $supply_name ) {
					return array(
						'@type' => 'HowToSupply',
						'name'  => (string) $supply_name,
					);
				},
				$supplies
			);
		}

		$tools = isset( $clean['tools'] ) && is_array( $clean['tools'] ) ? $clean['tools'] : array();
		if ( ! empty( $tools ) ) {
			$node['tool'] = array_map(
				static function ( $tool_name ) {
					return array(
						'@type' => 'HowToTool',
						'name'  => (string) $tool_name,
					);
				},
				$tools
			);
		}

		/**
		 * Filter a block-derived HowTo node before emission.
		 *
		 * @param array    $node      HowTo node.
		 * @param \WP_Post $post      Post.
		 * @param array    $clean     Sanitized attributes.
		 * @param string   $block_ref Source reference.
		 */
		$filtered = apply_filters( 'seovaultseo_howto_block_schema_node', $node, $post, $clean, $block_ref );
		return is_array( $filtered ) ? $filtered : $node;
	}

	/**
	 * @param array $clean Sanitized attributes.
	 * @return bool
	 */
	private static function block_has_any_content( array $clean ) {
		if ( '' !== trim( (string) ( $clean['name'] ?? '' ) ) ) {
			return true;
		}
		if ( '' !== trim( (string) ( $clean['description'] ?? '' ) ) ) {
			return true;
		}
		if ( ! empty( $clean['steps'] ) || ! empty( $clean['supplies'] ) || ! empty( $clean['tools'] ) ) {
			return true;
		}
		if ( '' !== trim( (string) ( $clean['totalTime'] ?? '' ) ) ) {
			return true;
		}
		$cost = isset( $clean['estimatedCost'] ) && is_array( $clean['estimatedCost'] ) ? $clean['estimatedCost'] : array();
		return '' !== trim( (string) ( $cost['currency'] ?? '' ) ) || '' !== trim( (string) ( $cost['value'] ?? '' ) );
	}

	/**
	 * @param array $steps Sanitized steps.
	 * @return bool
	 */
	private static function has_partial_steps( array $steps ) {
		foreach ( $steps as $step ) {
			if ( ! is_array( $step ) ) {
				continue;
			}
			$name  = isset( $step['name'] ) ? trim( (string) $step['name'] ) : '';
			$plain = isset( $step['text_plain'] )
				? trim( (string) $step['text_plain'] )
				: HowToBlockAttributeSanitizer::normalize_plain_text( $step['text'] ?? '' );
			if ( '' === $name && '' === $plain ) {
				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 ) {
		$name    = trim( (string) ( $clean['name'] ?? '' ) );
		$steps   = HowToBlockAttributeSanitizer::get_complete_steps( $clean );
		$setup   = array();
		$blockers = array();

		if ( '' === $name ) {
			$setup[]    = array(
				'code'       => 'readiness.howto.name_missing',
				'kind'       => 'blocker',
				'field_key'  => 'name',
				'action_key' => 'add_howto_name',
			);
			$blockers[] = array(
				'code'       => 'readiness.howto.name_missing',
				'field_key'  => 'name',
				'action_key' => 'add_howto_name',
			);
		}
		if ( empty( $steps ) ) {
			$setup[]    = array(
				'code'       => 'readiness.howto.steps_missing',
				'kind'       => 'blocker',
				'field_key'  => 'step',
				'action_key' => 'add_howto_step',
			);
			$blockers[] = array(
				'code'       => 'readiness.howto.steps_missing',
				'field_key'  => 'step',
				'action_key' => 'add_howto_step',
			);
		}

		return array(
			'type'        => 'HowTo',
			'@id'         => '',
			'source'      => self::SOURCE,
			'source_ref'  => $source_ref,
			'emitted'     => false,
			'withheld'    => true,
			'reason'      => 'howto_block_incomplete',
			'graph_index' => -1,
			'block_index' => (int) $index,
			'name'        => $name,
			'setup'       => $setup,
			'readiness'   => array(
				'ready'    => false,
				'blockers' => $blockers,
			),
		);
	}

	/**
	 * @param string $content   Post content.
	 * @param array  $instances Emitted instances.
	 * @return string
	 */
	private static function fingerprint_content( $content, array $instances ) {
		$payload = array(
			'content_sha' => hash( 'sha256', (string) $content ),
			'instances'   => array_map(
				static function ( $row ) {
					$steps = isset( $row['steps'] ) && is_array( $row['steps'] ) ? $row['steps'] : array();
					return array(
						'id'    => (string) ( $row['@id'] ?? '' ),
						'name'  => (string) ( $row['name'] ?? '' ),
						'steps' => array_map(
							static function ( $step ) {
								return array(
									'n' => (string) ( $step['name'] ?? '' ),
									't' => (string) ( $step['text_plain'] ?? '' ),
								);
							},
							$steps
						),
					);
				},
				$instances
			),
		);
		$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(
			'nodes'                => array(),
			'instances'            => 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,
				'instance_count'     => 0,
			),
		);
	}
}
