<?php
/**
 * GenerateBlocks integration: replace dynamic tags in SmartCrawl meta title and description.
 *
 * @package SmartCrawl
 */

namespace SmartCrawl\Integration\Generateblocks;

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

/**
 * Replaces GenerateBlocks dynamic tag placeholders in SmartCrawl meta output.
 */
class Meta_Tags {

	const RENDER_BLOCK_DATA_KEY    = '_wds_rb_data';
	const REPLACED_DESCRIPTION_KEY = '_wds_gbrpl';
	const REPLACED_TITLE_KEY       = '_wds_gbrtl';

	/**
	 * Boots the integration.
	 *
	 * @return void
	 */
	public static function run() {
		if ( ! defined( 'SMARTCRAWL_VERSION' ) || ! defined( 'GENERATEBLOCKS_VERSION' ) ) {
			return;
		}

		add_filter( 'render_block', array( __CLASS__, 'capture_block_context' ), 10, 3 );
		add_filter( 'smartcrawl_get_meta_description', array( __CLASS__, 'replace_meta_description_tags' ), 10, 1 );
		add_filter( 'smartcrawl_get_meta_title', array( __CLASS__, 'replace_meta_title_tags' ), 10, 1 );
	}

	/**
	 * Captures block context on render for use when resolving meta later (e.g. in wp_head).
	 *
	 * @param string $content Block content.
	 * @param array $block Full block (name, attrs, etc.).
	 * @param object $instance Block instance (WP_Block).
	 *
	 * @return string Unchanged block content.
	 */
	public static function capture_block_context( $content, $block, $instance ) {
		$GLOBALS[ self::RENDER_BLOCK_DATA_KEY ] = array( $content, $block, $instance );

		return $content;
	}

	/**
	 * Filter callback for SmartCrawl meta description: replaces GenerateBlocks dynamic tags
	 *
	 * @param string $description The meta description string (may contain {{tag}} placeholders from autogenerated excerpt/content).
	 *
	 * @return string Meta description with all GenerateBlocks dynamic tags replaced by their resolved values.
	 */
	public static function replace_meta_description_tags( $description ) {
		return self::replace_meta_value( $description, self::REPLACED_DESCRIPTION_KEY );
	}

	/**
	 * Filter callback for SmartCrawl meta title: replaces GenerateBlocks dynamic tags
	 *
	 * @param string $title The meta title string (may contain {{tag}} placeholders from title template or autogenerated title).
	 *
	 * @return string Meta title with all GenerateBlocks dynamic tags replaced by their resolved values.
	 */
	public static function replace_meta_title_tags( $title ) {
		return self::replace_meta_value( $title, self::REPLACED_TITLE_KEY );
	}

	/**
	 * Replaces dynamic tags in a meta value and caches the result per request.
	 *
	 * @param string $value Meta description or title (may contain {{tag}} placeholders).
	 * @param string $cache_key Global key used to cache the result for the request.
	 *
	 * @return string Value with placeholders replaced.
	 */
	private static function replace_meta_value( $value, $cache_key ) {
		if ( isset( $GLOBALS[ $cache_key ] ) ) {
			return $GLOBALS[ $cache_key ];
		}

		$block_data = $GLOBALS[ self::RENDER_BLOCK_DATA_KEY ] ?? self::get_fallback_block_data();

		$replaced              = self::replace_dynamic_tags( $value, $block_data );
		$GLOBALS[ $cache_key ] = $replaced;

		return $replaced;
	}

	/**
	 * Builds block context from the current post when meta is output before any block is rendered (e.g. in wp_head).
	 *
	 * @return array|null [ content, block, instance ] or null when not on a singular post.
	 */
	private static function get_fallback_block_data() {
		if ( ! is_singular() ) {
			return null;
		}

		$post_id = get_queried_object_id();
		if ( ! $post_id ) {
			return null;
		}

		$post = get_post( $post_id );
		if ( ! $post instanceof \WP_Post ) {
			return null;
		}

		$block = self::get_first_valid_block( $post->post_content );
		if ( ! $block ) {
			$block = array(
				'blockName'    => 'generateblocks/headline',
				'attrs'        => array(),
				'innerBlocks'  => array(),
				'innerContent' => array(),
			);
		}

		$context = array(
			'postId'   => $post_id,
			'postType' => $post->post_type,
		);

		return array( '', $block, new \WP_Block( $block, $context ) );
	}

	/**
	 * Returns the first parsed block that has a blockName (skips empty/whitespace).
	 *
	 * @param string $post_content Post content to parse.
	 *
	 * @return array|null Block array or null.
	 */
	private static function get_first_valid_block( $post_content ) {
		$blocks = parse_blocks( $post_content );
		foreach ( $blocks as $parsed ) {
			if ( ! empty( $parsed['blockName'] ) && is_string( $parsed['blockName'] ) ) {
				return $parsed;
			}
		}

		return null;
	}

	/**
	 * Replaces dynamic tags in a string using block context (plugin API, then registry fallback).
	 *
	 * @param string $value String that may contain {{tag}} placeholders.
	 * @param array|null $block_data [ content, block, instance ] or null.
	 *
	 * @return string Value with placeholders replaced where possible.
	 */
	private static function replace_dynamic_tags( $value, $block_data ) {
		if ( ! is_array( $block_data ) || count( $block_data ) < 3 ) {
			return $value;
		}

		if ( ! class_exists( 'GenerateBlocks_Register_Dynamic_Tag' ) ||
			 ! method_exists( 'GenerateBlocks_Register_Dynamic_Tag', 'replace_tags' ) ) {
			return $value;
		}

		list( , $block, $instance ) = $block_data;
		$replaced = \GenerateBlocks_Register_Dynamic_Tag::replace_tags( $value, $block, $instance );

		if ( is_singular() ) {
			$replaced = self::replace_remaining_placeholders( $replaced, $block_data );
		}

		return $replaced;
	}

	/**
	 * Replaces any remaining {{tag}} placeholders using GenerateBlocks' registry (reuses existing block_data).
	 *
	 * @param string $value Value that may still contain placeholders after replace_tags().
	 * @param array|null $block_data [ content, block, instance ] from caller; reused to avoid double get_fallback_block_data().
	 *
	 * @return string Value with placeholders replaced via plugin callbacks.
	 */
	private static function replace_remaining_placeholders( $value, $block_data ) {
		if ( false === strpos( $value, '{{' ) ) {
			return $value;
		}

		if ( ! self::is_registry_available() ) {
			return $value;
		}

		$block_data = is_array( $block_data ) && count( $block_data ) >= 3
			? $block_data
			: self::get_fallback_block_data();

		if ( ! is_array( $block_data ) || count( $block_data ) < 3 ) {
			return $value;
		}

		$tags = \GenerateBlocks_Register_Dynamic_Tag::get_tags();
		if ( empty( $tags ) ) {
			return $value;
		}

		list( , $block, $instance ) = $block_data;
		$matches = \GenerateBlocks_Register_Dynamic_Tag::find_matches( $value, $tags );

		foreach ( $matches as $match ) {
			$tag_name       = $match[1] ?? '';
			$full_tag       = $match[0] ?? '';
			$options_string = isset( $match[2] ) ? ltrim( $match[2], ' ' ) : '';

			if ( empty( $tag_name ) || ! isset( $tags[ $tag_name ]['return'] ) || ! is_callable( $tags[ $tag_name ]['return'] ) ) {
				continue;
			}

			$full_tag    = \GenerateBlocks_Register_Dynamic_Tag::maybe_prepend_protocol( $value, $full_tag );
			$options     = \GenerateBlocks_Register_Dynamic_Tag::parse_options( $options_string, $tag_name );
			$replacement = $tags[ $tag_name ]['return']( $options, $block, $instance );
			$value       = str_replace( $full_tag, (string) $replacement, $value );
		}

		return $value;
	}

	/**
	 * Whether GenerateBlocks' dynamic tag registry can be used (class and required methods exist).
	 *
	 * @return bool
	 */
	private static function is_registry_available() {
		if ( ! class_exists( 'GenerateBlocks_Register_Dynamic_Tag' ) ) {
			return false;
		}
		$class = 'GenerateBlocks_Register_Dynamic_Tag';

		return method_exists( $class, 'get_tags' )
			   && method_exists( $class, 'find_matches' )
			   && method_exists( $class, 'parse_options' )
			   && method_exists( $class, 'maybe_prepend_protocol' );
	}
}
