<?php
/**
 * Changeloger Frontend Handler
 *
 * @package Changeloger
 */

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

/**
 * Class Changeloger_Frontend
 *
 * Handles frontend template, blocks for changelog display
 */
class Changeloger_Frontend
{

	/**
	 * Transient key for release hub pages cache
	 */
	const RELEASE_HUB_PAGES_TRANSIENT = 'cha_release_hub_pages_cache';

	/**
	 * Initialize the class
	 */
	public static function init()
	{
		add_filter('template_include', [__CLASS__, 'changelog_template']);
		add_filter('single_template', [__CLASS__, 'single_release_template']);
		add_filter('theme_page_templates', [__CLASS__, 'register_page_template']);
		add_action('wp_enqueue_scripts', [__CLASS__, 'enqueue_frontend_assets']);
		add_action('init', [__CLASS__, 'register_release_hub_block']);
		add_action('enqueue_block_editor_assets', [__CLASS__, 'localize_block_editor_data']);
		add_action('save_post', [__CLASS__, 'clear_release_hub_cache'], 10, 2);
		add_action('template_redirect', [__CLASS__, 'handle_embed_request']);

		// AJAX badge filter (public — frontend, no auth required).
		add_action( 'wp_ajax_cha_filter_releases', [ __CLASS__, 'ajax_filter_releases' ] );
		add_action( 'wp_ajax_nopriv_cha_filter_releases', [ __CLASS__, 'ajax_filter_releases' ] );
	}

	/**
	 * AJAX handler for badge filter and pagination.
	 *
	 * Renders the releases container + pagination HTML partial
	 * and returns JSON { html, currentPage, totalPages }.
	 */
	public static function ajax_filter_releases() {
		// Sanitize inputs.
		$product_id   = absint( $_POST['product_id'] ?? 0 );
		$badge        = sanitize_text_field( $_POST['badge'] ?? '' );
		$page         = max( 1, absint( $_POST['page'] ?? 1 ) );
		$limit        = absint( $_POST['limit'] ?? 0 );
		$order        = in_array( ( $_POST['order'] ?? 'DESC' ), [ 'ASC', 'DESC' ], true )
			? sanitize_text_field( $_POST['order'] ) : 'DESC';
		$layout       = sanitize_text_field( $_POST['layout'] ?? 'default' );
		$width_mode   = sanitize_text_field( $_POST['width_mode'] ?? 'default' );
		$custom_width = absint( $_POST['custom_width'] ?? 800 );

		// Check Transient Cache
		$cache_version = get_option( 'cha_releases_cache_version', '1' );
		$cache_key     = 'cha_ajax_rel_' . md5( wp_json_encode( $_POST ) . $cache_version );
		$cached_data   = get_transient( $cache_key );

		if ( false !== $cached_data ) {
			wp_send_json_success( $cached_data );
		}

		$settings      = get_option( 'changeloger_settings', [] );
		$default_limit = ! empty( $settings['items_per_page'] ) ? absint( $settings['items_per_page'] ) : -1;

		if ( ! $limit ) {
			$limit = $default_limit;
		}

		$paginate   = $limit > 0;
		$query_args = [
			'post_type'      => Changeloger_Post_Types::POST_TYPE,
			'posts_per_page' => $paginate ? $limit : -1,
			'post_status'    => 'publish',
			'orderby'        => 'meta_value',
			'meta_key'       => Changeloger_Release_Meta::META_RELEASE_DATE,
			'order'          => $order,
			'no_found_rows'  => ! $paginate,
		];

		if ( $paginate ) {
			$query_args['paged'] = $page;
		}

		if ( $product_id ) {
			$query_args['tax_query'] = [
				[
					'taxonomy' => Changeloger_Post_Types::TAXONOMY_PRODUCT,
					'field'    => 'term_id',
					'terms'    => $product_id,
				],
			];
		}

		if ( $badge ) {
			$query_args['meta_query'] = [
				[
					'key'   => '_cha_badge_slug',
					'value' => $badge,
				],
			];
		} else {
			// Handle "Show All" pill disabled scenario.
			$show_all_pill = $settings['badge_filter_show_all'] ?? true;
			if ( ! $show_all_pill ) {
				$default_badge = ! empty( $settings['badge_filter_default'] ) ? $settings['badge_filter_default'] : '';
				if ( ! $default_badge && class_exists( 'Changeloger_Post_Types' ) ) {
					$all_badges    = Changeloger_Post_Types::get_release_badges();
					$default_badge = ! empty( $all_badges ) ? $all_badges[0]['slug'] : '';
				}
				if ( $default_badge ) {
					$query_args['meta_query'] = [
						[
							'key'   => '_cha_badge_slug',
							'value' => $default_badge,
						],
					];
				}
			}
		}

		$releases = new WP_Query( $query_args );

		$args = [
			'layout'       => $layout,
			'width_mode'   => $width_mode,
			'custom_width' => $custom_width,
		];

		ob_start();
		self::render_releases_content( $releases, $args, $page );
		$html = ob_get_clean();

		$response_data = [
			'html'        => $html,
			'currentPage' => $page,
			'totalPages'  => (int) $releases->max_num_pages,
		];

		wp_reset_postdata();

		// Cache for 12 hours (invalidated via cha_releases_cache_version on save).
		set_transient( $cache_key, $response_data, 12 * HOUR_IN_SECONDS );

		wp_send_json_success( $response_data );
	}

	/**
	 * Register Release Hub block
	 */
	public static function register_release_hub_block()
	{
		register_block_type(plugin_dir_path(dirname(__FILE__)) . 'build/release-hub', [
			'render_callback' => [__CLASS__, 'render_release_hub_block'],
		]);
	}

	/**
	 * Localize admin URL data for the Release Hub block editor script.
	 *
	 * Passes the WordPress admin URL so editor tab panels can link
	 * to the correct Releases, Feedback, and Roadmap admin pages.
	 *
	 * @return void
	 */
	public static function localize_block_editor_data(): void {
		wp_add_inline_script(
			'cha-release-hub-editor-script',
			'window.chaReleaseHubAdmin = ' . wp_json_encode( [
				'adminUrl' => admin_url(),
			] ) . ';',
			'before'
		);
	}

	/**
	 * Render Release Hub block
	 *
	 * Handles all block-level attributes for tab labels, default tab,
	 * releases per page, layout style, and section visibility.
	 */
	public static function render_release_hub_block( $attributes, $content ) {
		$product_id      = ! empty( $attributes['productId'] ) ? absint( $attributes['productId'] ) : 0;
		$show_releases   = isset( $attributes['showReleases'] ) ? (bool) $attributes['showReleases'] : true;
		$show_feedback   = isset( $attributes['showFeedback'] ) ? (bool) $attributes['showFeedback'] : true;
		$show_roadmap    = isset( $attributes['showRoadmap'] ) ? (bool) $attributes['showRoadmap'] : true;
		$default_tab     = ! empty( $attributes['defaultTab'] ) ? sanitize_key( $attributes['defaultTab'] ) : 'releases';
		$releases_limit  = ! empty( $attributes['releasesPerPage'] ) ? absint( $attributes['releasesPerPage'] ) : 0;
		$releases_label  = ! empty( $attributes['releasesLabel'] ) ? sanitize_text_field( $attributes['releasesLabel'] ) : '';
		$feedback_label  = ! empty( $attributes['feedbackLabel'] ) ? sanitize_text_field( $attributes['feedbackLabel'] ) : '';
		$roadmap_label   = ! empty( $attributes['roadmapLabel'] ) ? sanitize_text_field( $attributes['roadmapLabel'] ) : '';
		$show_header     = isset( $attributes['showHeader'] ) ? (bool) $attributes['showHeader'] : true;
		$show_badge_filter = isset( $attributes['showBadgeFilter'] ) ? (bool) $attributes['showBadgeFilter'] : true;
		$layout          = ! empty( $attributes['layout'] ) ? sanitize_key( $attributes['layout'] ) : 'default';

		// Per-tab container width: mode (default|full|custom) + custom px value.
		$releases_width_mode   = ! empty( $attributes['releasesWidthMode'] ) ? sanitize_key( $attributes['releasesWidthMode'] ) : 'default';
		$releases_custom_width = ! empty( $attributes['releasesCustomWidth'] ) ? absint( $attributes['releasesCustomWidth'] ) : 800;
		$feedback_width_mode   = ! empty( $attributes['feedbackWidthMode'] ) ? sanitize_key( $attributes['feedbackWidthMode'] ) : 'default';
		$feedback_custom_width = ! empty( $attributes['feedbackCustomWidth'] ) ? absint( $attributes['feedbackCustomWidth'] ) : 800;
		$roadmap_width_mode    = ! empty( $attributes['roadmapWidthMode'] ) ? sanitize_key( $attributes['roadmapWidthMode'] ) : 'default';
		$roadmap_custom_width  = ! empty( $attributes['roadmapCustomWidth'] ) ? absint( $attributes['roadmapCustomWidth'] ) : 800;

		// Get plugin settings.
		$settings = get_option( 'changeloger_settings', [] );

		// Combine block-level toggles with global settings.
		$is_premium        = function_exists( 'cha_is_premium' ) && cha_is_premium();
		$feedback_enabled  = $show_feedback && ( ! isset( $settings['enable_feedback'] ) || ! empty( $settings['enable_feedback'] ) );
		$roadmap_enabled   = $show_roadmap && $is_premium && ( ! isset( $settings['enable_roadmap'] ) || ! empty( $settings['enable_roadmap'] ) );
		$releases_enabled  = $show_releases;

		// If nothing is enabled, render nothing.
		if ( ! $releases_enabled && ! $feedback_enabled && ! $roadmap_enabled ) {
			return '';
		}

		// Enqueue release hub tab-switching assets (always needed when 2+ sections).
		wp_enqueue_style(
			'cha-release-hub-css',
			plugins_url( 'assets/css/release-hub.css', dirname( __FILE__ ) ),
			[],
			'1.0.0'
		);

		// Only enqueue releases-specific assets when releases section is enabled.
		if ( $releases_enabled ) {
			wp_enqueue_style(
				'cha-frontend-css',
				plugins_url( 'assets/css/frontend-changelog.css', dirname( __FILE__ ) ),
				[],
				'1.0.0'
			);
			wp_enqueue_script(
				'cha-release-hub-js',
				plugins_url( 'assets/js/release-hub.js', dirname( __FILE__ ) ),
				[],
				'1.0.0',
				true
			);

			// Excerpt expand ("Continue Reading") and change-section accordion toggles.
			wp_enqueue_script(
				'cha-frontend-js',
				plugins_url( 'assets/js/frontend-changelog.js', dirname( __FILE__ ) ),
				[],
				'1.0.0',
				true
			);

			// Localize script.
			if ( class_exists( 'Changeloger_Block_Assets' ) ) {
				Changeloger_Block_Assets::localize_script( 'cha-release-hub-js' );
			}
		}

		// Enqueue feedback assets if section is active.
		if ( $feedback_enabled ) {
			self::enqueue_feedback_assets( $settings, $product_id );
		}

		// Note: Roadmap frontend assets are conditionally enqueued by changeloger-pro

		// Enqueue WordPress editor for WYSIWYG functionality (if premium).
		if ( $is_premium ) {
			wp_enqueue_editor();
		}


		// Count active sections to decide whether to show tabs.
		$active_count = (int) $releases_enabled + (int) $feedback_enabled + (int) $roadmap_enabled;

		// Determine the first active tab, respecting the default tab attribute.
		$first_active = 'releases';
		$tab_order    = [ $default_tab, 'releases', 'feedback', 'roadmap' ];
		foreach ( $tab_order as $tab ) {
			if ( 'releases' === $tab && $releases_enabled ) {
				$first_active = 'releases';
				break;
			} elseif ( 'feedback' === $tab && $feedback_enabled ) {
				$first_active = 'feedback';
				break;
			} elseif ( 'roadmap' === $tab && $roadmap_enabled ) {
				$first_active = 'roadmap';
				break;
			}
		}

		// Resolve display labels (fall back to defaults).
		$releases_display_label = $releases_label ?: esc_html__( 'Releases', 'changeloger' );
		$feedback_display_label = $feedback_label ?: esc_html__( 'Feedback', 'changeloger' );
		$roadmap_display_label  = $roadmap_label ?: esc_html__( 'Roadmap', 'changeloger' );

		// Build layout CSS class.
		$hub_classes = 'cha-release-hub';
		if ( 'compact' === $layout ) {
			$hub_classes .= ' cha-release-hub--compact';
		}

		ob_start();
		?>
		<div class="<?php echo esc_attr( $hub_classes ); ?>"
			data-default-tab="<?php echo esc_attr( $default_tab ); ?>"
			data-show-header="<?php echo $show_header ? '1' : '0'; ?>"
			data-show-badge-filter="<?php echo $show_badge_filter ? '1' : '0'; ?>">
			<?php if ( $active_count >= 2 ) : ?>
				<!-- Tabs Navigation -->
				<div class="cha-hub-tabs">
					<?php if ( $releases_enabled ) : ?>
						<button type="button" class="cha-hub-tab<?php echo 'releases' === $first_active ? ' active' : ''; ?>" data-tab="releases">
							<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="16" height="16">
								<path fill-rule="evenodd"
									d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm2 10a1 1 0 10-2 0v3a1 1 0 102 0v-3zm2-3a1 1 0 011 1v5a1 1 0 11-2 0v-5a1 1 0 011-1zm4-1a1 1 0 10-2 0v7a1 1 0 102 0V8z"
									clip-rule="evenodd" />
							</svg>
							<?php echo esc_html( $releases_display_label ); ?>
						</button>
					<?php endif; ?>
					<?php if ( $feedback_enabled ) : ?>
						<button type="button" class="cha-hub-tab<?php echo 'feedback' === $first_active ? ' active' : ''; ?>" data-tab="feedback">
							<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="16" height="16">
								<path fill-rule="evenodd"
									d="M18 13V5a2 2 0 00-2-2H4a2 2 0 00-2 2v8a2 2 0 002 2h3l3 3 3-3h3a2 2 0 002-2zM5 7a1 1 0 011-1h8a1 1 0 110 2H6a1 1 0 01-1-1zm1 3a1 1 0 100 2h3a1 1 0 100-2H6z"
									clip-rule="evenodd" />
							</svg>
							<?php echo esc_html( $feedback_display_label ); ?>
						</button>
					<?php endif; ?>
					<?php if ( $roadmap_enabled ) : ?>
						<button type="button" class="cha-hub-tab<?php echo 'roadmap' === $first_active ? ' active' : ''; ?>" data-tab="roadmap">
							<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="16" height="16">
								<path fill-rule="evenodd"
									d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
									clip-rule="evenodd" />
							</svg>
							<?php echo esc_html( $roadmap_display_label ); ?>
						</button>
					<?php endif; ?>
				</div>
			<?php endif; ?>

			<!-- Tab Panels -->
			<div class="cha-hub-content">
				<?php if ( $releases_enabled ) : ?>
					<!-- Releases Panel -->
					<div class="cha-hub-panel<?php echo 'releases' === $first_active ? ' active' : ''; ?>"
						data-panel="releases">
						<?php
						self::render_changelog( [
							'product_id'        => $product_id,
							'limit'             => $releases_limit > 0 ? $releases_limit : null,
							'show_header'       => $show_header,
							'show_badge_filter' => $show_badge_filter,
							'layout'            => $layout,
							'width_mode'        => $releases_width_mode,
							'custom_width'      => $releases_custom_width,
						] );
						?>
					</div>
				<?php endif; ?>

				<?php if ( $feedback_enabled ) : ?>
					<!-- Feedback Panel - React App Container -->
					<div class="cha-hub-panel<?php echo 'feedback' === $first_active ? ' active' : ''; ?>"
						data-panel="feedback">
						<div id="cha-feedback-app" data-product-id="<?php echo esc_attr( $product_id ); ?>"<?php echo self::get_container_width_style( $feedback_width_mode, $feedback_custom_width ); ?>></div>
					</div>
				<?php endif; ?>

				<?php if ( $roadmap_enabled ) : ?>
					<!-- Roadmap Panel - React App Container -->
					<div class="cha-hub-panel<?php echo 'roadmap' === $first_active ? ' active' : ''; ?>"
						data-panel="roadmap">
						<div id="cha-roadmap-root" data-product="<?php echo esc_attr( $product_id ); ?>"<?php echo self::get_container_width_style( $roadmap_width_mode, $roadmap_custom_width ); ?>></div>
					</div>
				<?php endif; ?>
			</div>

			<?php if ( ! $is_premium ) : ?>
				<!-- Powered by badge (free plugin only) -->
				<div class="cha-powered-by">
					<a href="http://changeloger.spider-themes.net/" target="_blank" rel="noopener noreferrer">
						<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20" width="14" height="14">
							<path fill-rule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clip-rule="evenodd" />
						</svg>
						<?php esc_html_e( 'Powered by Changeloger', 'changeloger' ); ?>
					</a>
				</div>
			<?php endif; ?>
		</div>
		<?php
		return ob_get_clean();
	}

	/**
	 * Generate inline style attribute for a container based on width mode.
	 *
	 * Returns a style="..." attribute string for direct use on HTML elements.
	 *
	 * @param string $mode         Width mode: 'default', 'full', or 'custom'.
	 * @param int    $custom_width Custom width value in pixels (used only when mode is 'custom').
	 *
	 * @return string Inline style attribute string, or empty string for default.
	 */
	private static function get_container_width_style( $mode, $custom_width ) {
		switch ( $mode ) {
			case 'full':
				return ' style="max-width:100%;"';
			case 'custom':
				return ' style="max-width:' . esc_attr( $custom_width ) . 'px;margin-left:auto;margin-right:auto;"';
			default:
				return '';
		}
	}

	/**
	 * Enqueue feedback frontend assets
	 *
	 * @param array $settings Plugin settings
	 * @param int   $product_id Product ID
	 */
	public static function enqueue_feedback_assets($settings, $product_id = 0)
	{
		$feedback_asset_file = plugin_dir_path(dirname(__FILE__)) . 'build/feedback.asset.php';
		$feedback_asset = file_exists($feedback_asset_file) ? include $feedback_asset_file : [
			'dependencies' => ['wp-element', 'wp-api-fetch'],
			'version' => CHANGELOGER_VERSION
		];

		wp_enqueue_script(
			'cha-feedback-frontend',
			plugins_url('build/feedback.js', dirname(__FILE__)),
			$feedback_asset['dependencies'],
			$feedback_asset['version'],
			true
		);

		wp_enqueue_style(
			'cha-feedback-frontend-css',
			plugins_url('build/feedback.css', dirname(__FILE__)),
			['cha-common-css'],
			$feedback_asset['version']
		);

		wp_localize_script('cha-feedback-frontend', 'chaFeedbackFrontend', [
			'productId' => $product_id,
			'restUrl' => rest_url('changeloger/v1/feedback'),
			'nonce' => wp_create_nonce('wp_rest'),
			'isLoggedIn' => is_user_logged_in(),
			'user' => is_user_logged_in() ? [
				'id' => get_current_user_id(),
				'name' => wp_get_current_user()->display_name,
				'avatar' => get_avatar_url(get_current_user_id(), ['size' => 48]),
				'roles' => (array) wp_get_current_user()->roles,
			] : null,
			'isPremium' => function_exists('cha_is_premium') && cha_is_premium(),
			'categories' => class_exists('Changeloger_Post_Types') ? Changeloger_Post_Types::get_feedback_categories() : [],
			'statuses' => self::get_feedback_status_config(),
			// Pagination — use the global items_per_page setting.
			'perPage' => ! empty( $settings['items_per_page'] ) ? absint( $settings['items_per_page'] ) : 10,
			// Feedback settings
			'enableFeedback' => !isset($settings['enable_feedback']) || !empty($settings['enable_feedback']),
			'allowComments' => !isset($settings['allow_comments']) || !empty($settings['allow_comments']),
			// Voting settings
			'enableDownvoting' => !isset($settings['enable_downvoting']) || !empty($settings['enable_downvoting']),
			'allowGuestVoting' => !isset($settings['allow_guest_voting']) || !empty($settings['allow_guest_voting']),
			// Auth settings
			'enableGoogleLogin' => !isset($settings['enable_google_login']) || !empty($settings['enable_google_login']),
			'enableEmailSignup' => !isset($settings['enable_email_signup']) || !empty($settings['enable_email_signup']),
			'loginUrl' => wp_login_url(),
			'registerUrl' => wp_registration_url(),
			'ajaxUrl' => admin_url('admin-ajax.php'),
		]);
	}

	/**
	 * Build feedback status config for JS localization.
	 *
	 * @return array Feedback status configuration.
	 */
	private static function get_feedback_status_config() {
		$config = [];

		// Add static 'in_review' status.
		$config['in_review'] = [
			'label' => __( 'In Review', 'changeloger' ),
			'color' => '#9333ea',
		];

		// Add dynamic roadmap statuses from settings.
		$columns = Changeloger_Post_Types::get_roadmap_columns();
		foreach ( $columns as $slug => $column ) {
			$config[ $slug ] = [
				'label' => $column['label'],
				'color' => ! empty( $column['color'] ) ? $column['color'] : '#6b7280',
			];
		}

		return $config;
	}


	/**
	 * Register page template with WordPress
	 */
	public static function register_page_template($templates)
	{
		$templates['changelog-template.php'] = __('Changelog', 'changeloger');
		return $templates;
	}

	/**
	 * Enqueue frontend assets
	 */
	public static function enqueue_frontend_assets()
	{
		$should_enqueue = self::is_changelog_page() || is_singular( Changeloger_Post_Types::POST_TYPE );

		if ( $should_enqueue ) {
			wp_enqueue_style(
				'cha-frontend-css',
				plugins_url('assets/css/frontend-changelog.css', dirname(__FILE__)),
				[],
				'1.0.0'
			);

			wp_enqueue_script(
				'cha-frontend-js',
				plugins_url( 'assets/js/frontend-changelog.js', dirname( __FILE__ ) ),
				[],
				'1.0.0',
				true
			);
		}

		wp_register_style(
			'cha-common-css',
			plugins_url('assets/css/cha-common.css', dirname(__FILE__)),
			[],
			CHANGELOGER_VERSION
		);
	}

	/**
	 * Check if current page is changelog page
	 */
	public static function is_changelog_page() {
		global $post;
		if ( ! $post ) {
			return false;
		}

		return has_shortcode( $post->post_content, 'changeloger_releases' )
			|| has_block( 'cha/release-hub', $post )
			|| get_page_template_slug( $post->ID ) === 'changelog-template.php';
	}

	/**
	 * Load custom template for changelog page
	 */
	public static function changelog_template($template)
	{
		global $post;

		if ($post && get_page_template_slug($post->ID) === 'changelog-template.php') {
			$plugin_template = plugin_dir_path(dirname(__FILE__)) . 'templates/changelog-template.php';
			if (file_exists($plugin_template)) {
				return $plugin_template;
			}
		}

		return $template;
	}

	/**
	 * Render changelog
	 *
	 * Accepts optional block-level overrides for items per page,
	 * subscribe header visibility, badge filter visibility, and layout.
	 * When a page limit is set, server-side pagination is rendered.
	 *
	 * @param array $args {
	 *     Optional. Render arguments.
	 *
	 *     @type int    $limit             Items per page, null to use global default.
	 *     @type string $order             Sort order, default 'DESC'.
	 *     @type int    $product_id        Product taxonomy term ID.
	 *     @type bool   $show_header       Whether to show the subscribe header.
	 *     @type bool   $show_badge_filter Whether to show badge filter pills.
	 *     @type string $layout            Layout style: 'default' or 'compact'.
	 *     @type string $width_mode        Container width mode: 'default', 'full', or 'custom'.
	 *     @type int    $custom_width      Custom width in pixels (used when width_mode is 'custom').
	 * }
	 */
	public static function render_changelog($args = [])
	{
		// Get default limit from settings
		$settings = get_option('changeloger_settings', []);
		$default_limit = !empty($settings['items_per_page']) ? absint($settings['items_per_page']) : -1;

		$defaults = [
			'limit'             => $default_limit,
			'order'             => 'DESC',
			'product_id'        => 0,
			'show_header'       => true,
			'show_badge_filter' => true,
			'layout'            => 'default',
			'width_mode'        => 'default',
			'custom_width'      => 800,
		];
		$args = wp_parse_args($args, $defaults);

		// If block-level limit was explicitly set, use it instead of global default.
		if ( isset( $args['limit'] ) && null !== $args['limit'] ) {
			$limit = absint( $args['limit'] );
		} else {
			$limit = $default_limit;
		}

		// Determine current page for pagination.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$current_page = ! empty( $_GET['cha_page'] ) ? max( 1, absint( $_GET['cha_page'] ) ) : 1;
		$paginate     = $limit > 0;

		$query_args = [
			'post_type'      => Changeloger_Post_Types::POST_TYPE,
			'posts_per_page' => $paginate ? $limit : -1,
			'post_status'    => 'publish',
			'orderby'        => 'meta_value',
			'meta_key'       => Changeloger_Release_Meta::META_RELEASE_DATE,
			'order'          => $args['order'],
		];

		if ( $paginate ) {
			$query_args['paged'] = $current_page;
		}

		// Filter by product if specified
		if (!empty($args['product_id'])) {
			$query_args['tax_query'] = [
				[
					'taxonomy' => Changeloger_Post_Types::TAXONOMY_PRODUCT,
					'field' => 'term_id',
					'terms' => $args['product_id'],
				],
			];
		}

		// Filter by badge if specified via URL parameter.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ! empty( $_GET['cha_badge'] ) ) {
			$badge_slug = sanitize_text_field( wp_unslash( $_GET['cha_badge'] ) );
			$query_args['meta_query'] = isset( $query_args['meta_query'] ) ? $query_args['meta_query'] : [];
			$query_args['meta_query'][] = [
				'key'   => '_cha_badge_slug',
				'value' => $badge_slug,
			];
		} elseif ( $args['show_badge_filter'] ) {
			// When "Show All" pill is disabled, auto-filter by default badge on initial load.
			$show_all_pill = isset( $settings['badge_filter_show_all'] ) ? $settings['badge_filter_show_all'] : true;
			if ( ! $show_all_pill ) {
				$default_badge = ! empty( $settings['badge_filter_default'] ) ? $settings['badge_filter_default'] : '';
				if ( ! $default_badge && class_exists( 'Changeloger_Post_Types' ) ) {
					$all_badges    = Changeloger_Post_Types::get_release_badges();
					$default_badge = ! empty( $all_badges ) ? $all_badges[0]['slug'] : '';
				}
				if ( $default_badge ) {
					$query_args['meta_query'] = isset( $query_args['meta_query'] ) ? $query_args['meta_query'] : [];
					$query_args['meta_query'][] = [
						'key'   => '_cha_badge_slug',
						'value' => $default_badge,
					];
				}
			}
		}

		$releases = new WP_Query($query_args);

		/**
		 * Action hook for Pro badge filter pills.
		 *
		 * Fires before the changelog container, allowing
		 * the Pro plugin to inject badge filter pills.
		 * Rendered before the "no releases" check so pills
		 * remain visible even when a filtered badge has zero results.
		 *
		 * @param WP_Query $releases  The releases query object.
		 * @param array    $settings  The plugin settings array.
		 */
		if ( $args['show_badge_filter'] ) {
			do_action( 'cha_before_changelog_container', $releases, $settings );
		}

		if (!$releases->have_posts()) {
			echo '<div class="cha-no-releases">' . esc_html__('No releases found.', 'changeloger') . '</div>';
			return;
		}

		/**
		 * Action hook for Pro changelog header widgets.
		 *
		 * Fires before the changelog container and badge filter pills,
		 * allowing the Pro plugin to inject a subscribe header or other widgets.
		 * Block-level args (show_header, show_badge_filter, layout) are passed
		 * so Pro can conditionally render based on per-block settings.
		 *
		 * @param WP_Query $releases  The releases query object.
		 * @param array    $settings  The plugin settings array.
		 * @param array    $args      The render arguments including product_id and block overrides.
		 */
		if ( $args['show_header'] ) {
			do_action( 'cha_changelog_header', $releases, $settings, $args );
		}

		self::render_releases_content( $releases, $args, $current_page );
		wp_reset_postdata();
	}

	/**
	 * Render releases container and pagination.
	 *
	 * Extracted from render_changelog() so it can be called by the AJAX handler
	 * to return only the releases + pagination HTML partial.
	 *
	 * @param WP_Query $releases     The releases query.
	 * @param array    $args         Render arguments (layout, width_mode, custom_width).
	 * @param int      $current_page Current page number.
	 */
	public static function render_releases_content( $releases, $args, $current_page ) {
		$container_classes = 'cha-changelog-container';
		if ( 'compact' === ( $args['layout'] ?? 'default' ) ) {
			$container_classes .= ' cha-changelog-container--compact';
		}

		// Hoist expensive lookups out of the per-item loop.
		$settings = get_option( 'changeloger_settings', [] );
		$badges   = Changeloger_Post_Types::get_release_badges();

		echo '<div class="' . esc_attr( $container_classes ) . '"' . self::get_container_width_style( $args['width_mode'] ?? 'default', $args['custom_width'] ?? 800 ) . '>';

		if ( $releases->have_posts() ) {
			while ( $releases->have_posts() ) {
				$releases->the_post();
				self::render_release_item( get_the_ID(), false, $args['layout'] ?? 'default', $settings, $badges );
			}
		} else {
			echo '<div class="cha-no-releases">' . esc_html__( 'No releases found.', 'changeloger' ) . '</div>';
		}

		echo '</div>';

		if ( $releases->max_num_pages > 1 ) {
			self::render_pagination( $current_page, $releases->max_num_pages );
		}
	}

	/**
	 * Render pagination navigation
	 *
	 * Outputs numbered page links with prev/next arrows.
	 * Uses the cha_page query parameter to avoid conflicts with WordPress core pagination.
	 *
	 * @param int $current_page Current page number.
	 * @param int $total_pages  Total number of pages.
	 */
	public static function render_pagination( $current_page, $total_pages ) {
		$base_url = remove_query_arg( 'cha_page' );

		echo '<nav class="cha-pagination" aria-label="' . esc_attr__( 'Releases pagination', 'changeloger' ) . '">';

		// Previous arrow.
		if ( $current_page > 1 ) {
			$prev_url = add_query_arg( 'cha_page', $current_page - 1, $base_url );
			echo '<a href="' . esc_url( $prev_url ) . '#releases" class="prev">';
			echo '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>';
			echo esc_html__( 'Prev', 'changeloger' );
			echo '</a>';
		} else {
			echo '<span class="prev disabled">';
			echo '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>';
			echo esc_html__( 'Prev', 'changeloger' );
			echo '</span>';
		}

		// Page numbers with dots.
		$range = 2; // Pages to show on each side of current.
		for ( $i = 1; $i <= $total_pages; $i++ ) {
			if ( $i === $current_page ) {
				echo '<span class="current">' . esc_html( $i ) . '</span>';
			} elseif ( $i === 1 || $i === $total_pages || abs( $i - $current_page ) <= $range ) {
				$page_url = 1 === $i ? $base_url : add_query_arg( 'cha_page', $i, $base_url );
				echo '<a href="' . esc_url( $page_url ) . '#releases">' . esc_html( $i ) . '</a>';
			} elseif ( $i === $current_page - $range - 1 || $i === $current_page + $range + 1 ) {
				echo '<span class="dots">…</span>';
			}
		}

		// Next arrow.
		if ( $current_page < $total_pages ) {
			$next_url = add_query_arg( 'cha_page', $current_page + 1, $base_url );
			echo '<a href="' . esc_url( $next_url ) . '#releases" class="next">';
			echo esc_html__( 'Next', 'changeloger' );
			echo '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>';
			echo '</a>';
		} else {
			echo '<span class="next disabled">';
			echo esc_html__( 'Next', 'changeloger' );
			echo '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>';
			echo '</span>';
		}

		echo '</nav>';
	}

	/**
	 * Render single release item
	 *
	 * @param int    $post_id   The release post ID.
	 * @param bool   $is_single Whether this is a single release page (no truncation).
	 * @param string $layout    Layout style: 'default' or 'compact'.
	 * @param array|null $settings  Pre-fetched plugin settings (avoids per-item get_option).
	 * @param array|null $badges    Pre-fetched release badges (avoids per-item lookup).
	 */
	public static function render_release_item( $post_id, $is_single = false, $layout = 'default', $settings = null, $badges = null )
	{
		$version = get_post_meta($post_id, Changeloger_Release_Meta::META_VERSION, true);
		$release_date = get_post_meta($post_id, Changeloger_Release_Meta::META_RELEASE_DATE, true);
		$change_items = get_post_meta($post_id, Changeloger_Release_Meta::META_CHANGE_ITEMS, true);
		$featured_image_id = get_post_meta( $post_id, Changeloger_Release_Meta::META_FEATURED_IMAGE, true );
		$title = get_the_title($post_id);
		$content = apply_filters('the_content', get_post_field('post_content', $post_id));

		// Lazy-load settings and badges only when not pre-passed (backward compatibility).
		if ( null === $settings ) {
			$settings = get_option( 'changeloger_settings', [] );
		}
		if ( null === $badges ) {
			$badges = Changeloger_Post_Types::get_release_badges();
		}

		// Check if content excerpt is enabled (only applies to list view).
		$excerpt_enabled = ! $is_single && ( ! isset( $settings['enable_content_excerpt'] ) || ! empty( $settings['enable_content_excerpt'] ) );

		// Get badge from post meta and lookup from pre-fetched badges.
		$badge      = null;
		$badge_slug = get_post_meta( $post_id, '_cha_badge_slug', true );
		if ( ! empty( $badge_slug ) ) {
			foreach ( $badges as $b ) {
				if ( $b['slug'] === $badge_slug ) {
					$badge = $b;
					break;
				}
			}
		}

		// Format date.
		$formatted_date = '';
		if ($release_date) {
			$date_obj = DateTime::createFromFormat('Y-m-d', $release_date);
			if ($date_obj) {
				$formatted_date = $date_obj->format('j M, Y');
			}
		}

		$change_groups = Changeloger_Post_Types::get_change_groups();
		$badge_slug_attr = !empty($badge_slug) ? $badge_slug : '';
		$permalink       = get_permalink( $post_id );

		// Determine if content is long enough to need truncation.
		$has_content     = ! empty( $content ) || ( ! empty( $change_items ) && is_array( $change_items ) );
		$should_truncate = $excerpt_enabled && $has_content;
		?>
		<div class="cha-release-item<?php echo $is_single ? ' cha-release-single' : ''; ?>" data-release-id="<?php echo esc_attr($post_id); ?>" data-badge="<?php echo esc_attr($badge_slug_attr); ?>">
			<!-- Left Column: Version & Date -->
			<div class="cha-release-left">
				<div class="cha-release-version">
					<span class="cha-version-badge">v<?php echo esc_html($version ?: '—'); ?></span>
					<?php if ($badge): ?>
						<span
							class="cha-type-badge"
							style="background-color: <?php echo esc_attr($badge['color']); ?>20; color: <?php echo esc_attr($badge['color']); ?>"><?php echo esc_html($badge['name']); ?></span>
					<?php endif; ?>
				</div>
				<?php if ($formatted_date): ?>
					<div class="cha-release-date"><?php echo esc_html($formatted_date); ?></div>
				<?php endif; ?>
			</div>

			<!-- Right Column: Content -->
			<div class="cha-release-right">
				<?php if ( ! empty( $featured_image_id ) ) : ?>
					<div class="cha-release-featured-image">
						<?php echo wp_get_attachment_image( (int) $featured_image_id, 'large', false, array( 'class' => 'cha-featured-img' ) ); ?>
					</div>
				<?php endif; ?>

				<?php if ( $is_single ) : ?>
					<h2 class="cha-release-title"><?php echo esc_html( $title ); ?></h2>
				<?php else : ?>
					<h2 class="cha-release-title"><a href="<?php echo esc_url( $permalink ); ?>"><?php echo esc_html( $title ); ?></a></h2>
				<?php endif; ?>

				<?php if ($content): ?>
					<?php if ( $should_truncate ) : ?>
						<div class="cha-release-excerpt-wrapper">
							<div class="cha-release-content">
								<?php echo wp_kses_post($content); ?>
							</div>
							<div class="cha-release-excerpt-overlay"></div>
							<button type="button" class="cha-excerpt-read-more">
								<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" width="16" height="16">
									<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
								</svg>
								<?php esc_html_e( 'Continue Reading', 'changeloger' ); ?>
							</button>
						</div>
					<?php else : ?>
						<div class="cha-release-content">
							<?php echo wp_kses_post($content); ?>
						</div>
					<?php endif; ?>
				<?php endif; ?>

				<?php if (!empty($change_items) && is_array($change_items)): ?>
					<?php foreach ($change_items as $group): ?>
						<?php if (!empty($group['items'])): ?>
							<div class="cha-change-section">
								<button type="button" class="cha-change-toggle" aria-expanded="false">
									<span class="cha-change-group-name">
										<?php
										$group_label = isset($change_groups[$group['group']]) ? $change_groups[$group['group']] : ucfirst($group['group']);
										echo esc_html($group_label);
										?>
										<span class="cha-change-count">(<?php echo count($group['items']); ?>)</span>
									</span>
									<svg class="cha-toggle-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
										stroke="currentColor">
										<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
									</svg>
								</button>
								<div class="cha-change-items" hidden>
									<ul>
										<?php foreach ($group['items'] as $item): ?>
											<li><?php echo esc_html($item); ?></li>
										<?php endforeach; ?>
									</ul>
								</div>
							</div>
						<?php endif; ?>
					<?php endforeach; ?>
				<?php endif; ?>

				<?php
				/**
				 * Fires after release change sections — used by Pro to render the interactions bar.
				 *
				 * @param int $post_id The release post ID.
				 */
				do_action( 'cha_after_release_content', $post_id );
				?>

			</div>
		</div>
		<?php
	}

	/**
	 * Load single release template
	 *
	 * Intercepts single cha_release requests and loads our custom template.
	 *
	 * @param string $template The path to the template file.
	 * @return string Modified template path.
	 */
	public static function single_release_template( $template )
	{
		if ( is_singular( Changeloger_Post_Types::POST_TYPE ) ) {
			$plugin_template = plugin_dir_path( dirname( __FILE__ ) ) . 'templates/single-release.php';
			if ( file_exists( $plugin_template ) ) {
				return $plugin_template;
			}
		}
		return $template;
	}

	/**
	 * Render a single release page
	 *
	 * Displays the full release with version sidebar and all content — no truncation.
	 *
	 * @param int $post_id The release post ID.
	 */
	public static function render_single_release( $post_id )
	{
		echo '<div class="cha-changelog-container cha-single-release-container">';
		self::render_release_item( $post_id, true );
		/**
		 * Fires after the release item on the single release page — used by Pro to render the comments section.
		 *
		 * @param int $post_id The release post ID.
		 */
		do_action( 'cha_single_release_after_content', $post_id );
		self::render_version_navigation( $post_id );
		echo '</div>';
	}

	/**
	 * Render previous/next version navigation on single release pages
	 *
	 * Queries adjacent releases ordered by release date meta to find the
	 * previous (newer) and next (older) versions relative to the current one.
	 *
	 * @param int $post_id The current release post ID.
	 */
	private static function render_version_navigation( $post_id ) {
		$current_date = get_post_meta( $post_id, Changeloger_Release_Meta::META_RELEASE_DATE, true );

		if ( empty( $current_date ) ) {
			return;
		}

		// Get the product term for this release so navigation stays within the same product.
		$product_terms = wp_get_post_terms( $post_id, Changeloger_Post_Types::TAXONOMY_PRODUCT, [ 'fields' => 'ids' ] );
		$product_id    = ! empty( $product_terms ) && ! is_wp_error( $product_terms ) ? $product_terms[0] : 0;

		$base_args = [
			'post_type'      => Changeloger_Post_Types::POST_TYPE,
			'posts_per_page' => 1,
			'post_status'    => 'publish',
			'orderby'        => 'meta_value',
			'meta_key'       => Changeloger_Release_Meta::META_RELEASE_DATE,
			'post__not_in'   => [ $post_id ],
			'fields'         => 'ids',
		];

		if ( $product_id ) {
			$base_args['tax_query'] = [
				[
					'taxonomy' => Changeloger_Post_Types::TAXONOMY_PRODUCT,
					'field'    => 'term_id',
					'terms'    => $product_id,
				],
			];
		}

		// Previous release = newer date (the one released after this one).
		$prev_args = array_merge( $base_args, [
			'order'      => 'ASC',
			'meta_query' => [
				[
					'key'     => Changeloger_Release_Meta::META_RELEASE_DATE,
					'value'   => $current_date,
					'compare' => '>',
					'type'    => 'DATE',
				],
			],
		] );

		// Next release = older date (the one released before this one).
		$next_args = array_merge( $base_args, [
			'order'      => 'DESC',
			'meta_query' => [
				[
					'key'     => Changeloger_Release_Meta::META_RELEASE_DATE,
					'value'   => $current_date,
					'compare' => '<',
					'type'    => 'DATE',
				],
			],
		] );

		$prev_query = new WP_Query( $prev_args );
		$next_query = new WP_Query( $next_args );

		$prev_id = $prev_query->have_posts() ? $prev_query->posts[0] : 0;
		$next_id = $next_query->have_posts() ? $next_query->posts[0] : 0;

		if ( ! $prev_id && ! $next_id ) {
			return;
		}

		echo '<nav class="cha-version-nav" aria-label="' . esc_attr__( 'Version navigation', 'changeloger' ) . '">';

		// Previous (newer) version — left side.
		if ( $prev_id ) {
			$prev_version = get_post_meta( $prev_id, Changeloger_Release_Meta::META_VERSION, true );
			$prev_url     = get_permalink( $prev_id );
			echo '<a href="' . esc_url( $prev_url ) . '" class="cha-version-nav__link cha-version-nav__prev">';
			echo '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" width="16" height="16"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>';
			echo '<span class="cha-version-nav__text">';
			echo '<span class="cha-version-nav__label">' . esc_html__( 'Previous Version', 'changeloger' ) . '</span>';
			echo '<span class="cha-version-nav__version">v' . esc_html( $prev_version ) . '</span>';
			echo '</span>';
			echo '</a>';
		} else {
			echo '<span class="cha-version-nav__placeholder"></span>';
		}

		// Next (older) version — right side.
		if ( $next_id ) {
			$next_version = get_post_meta( $next_id, Changeloger_Release_Meta::META_VERSION, true );
			$next_url     = get_permalink( $next_id );
			echo '<a href="' . esc_url( $next_url ) . '" class="cha-version-nav__link cha-version-nav__next">';
			echo '<span class="cha-version-nav__text">';
			echo '<span class="cha-version-nav__label">' . esc_html__( 'Next Version', 'changeloger' ) . '</span>';
			echo '<span class="cha-version-nav__version">v' . esc_html( $next_version ) . '</span>';
			echo '</span>';
			echo '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" width="16" height="16"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>';
			echo '</a>';
		} else {
			echo '<span class="cha-version-nav__placeholder"></span>';
		}

		echo '</nav>';

		wp_reset_postdata();
	}



	/**
	 * Get the URL of the page containing the release-hub block
	 *
	 * @param int $product_id Optional product ID to filter by.
	 * @return string Page URL or empty string if not found.
	 */
	public static function get_release_hub_page_url($product_id = 0)
	{
		// Try to get from persistent map first (O(1) lookup).
		$map = get_option('cha_release_hub_map', false);

		if (false !== $map) {
			if (is_array($map)) {
				// Exact match.
				if (isset($map[$product_id]) && 'publish' === get_post_status($map[$product_id])) {
					return get_permalink($map[$product_id]);
				}
				// Generic match (product ID 0).
				if (isset($map[0]) && 'publish' === get_post_status($map[0])) {
					return get_permalink($map[0]);
				}
				// Backward compatibility: If no product specified (0) and no generic hub found,
				// return the first available hub if any exist.
				if (0 === $product_id && !empty($map)) {
					$first = reset($map);
					if ('publish' === get_post_status($first)) {
						return get_permalink($first);
					}
				}

				// Map exists but no match found - return empty to avoid query.
				return '';
			}
		}

		// Fallback: Run the query (once until cleared).
		global $wpdb;

		$sql = "SELECT ID, post_content FROM {$wpdb->posts}
				WHERE post_content LIKE '%wp:cha/release-hub%'
				AND post_status = 'publish'
				LIMIT 10";

		$results = $wpdb->get_results($sql);

		// Build and save map.
		$new_map = [];
		foreach ($results as $post) {
			if (preg_match('/wp:cha\/release-hub\s*(\{[^}]*\})?/', $post->post_content, $matches)) {
				$attrs = isset($matches[1]) ? json_decode($matches[1], true) : [];
				$block_product = isset($attrs['productId']) ? (int) $attrs['productId'] : 0;

				// Store first occurrence.
				if (!isset($new_map[$block_product])) {
					$new_map[$block_product] = $post->ID;
				}
			}
		}

		update_option('cha_release_hub_map', $new_map);

		// Return result from new map.
		if (isset($new_map[$product_id])) {
			return get_permalink($new_map[$product_id]);
		}
		if (isset($new_map[0])) {
			return get_permalink($new_map[0]);
		}

		// Backward compatibility: If no product specified (0) and no generic hub found,
		// return the first available hub if any exist.
		if (0 === $product_id && !empty($new_map)) {
			$first = reset($new_map);
			return get_permalink($first);
		}

		return '';
	}

	/**
	 * Clear release hub pages cache
	 *
	 * Only clears cache if the saved post is relevant (contains the block or is in cache).
	 *
	 * @param int     $post_id Post ID.
	 * @param WP_Post $post    Post object.
	 */
	public static function clear_release_hub_cache($post_id = 0, $post = null)
	{
		if (!$post && $post_id) {
			$post = get_post($post_id);
		}

		if (!$post) {
			return;
		}

		// Skip auto-drafts and revisions.
		if (in_array($post->post_status, ['auto-draft', 'inherit'], true)) {
			return;
		}

		$is_relevant = false;

		// Check if post contains the block.
		if (strpos($post->post_content, 'wp:cha/release-hub') !== false) {
			$is_relevant = true;
		}

		// Check if post ID is in current map.
		if (!$is_relevant) {
			$map = get_option('cha_release_hub_map', []);
			if (is_array($map) && in_array($post_id, $map)) {
				$is_relevant = true;
			}
		}

		if ($is_relevant) {
			delete_option('cha_release_hub_map');
			delete_transient(self::RELEASE_HUB_PAGES_TRANSIENT);
			// Invalidate the AJAX filter transients.
			update_option('cha_releases_cache_version', time());
		}
	}

	/**
	 * Handle embed request
	 * Detects ?cha_embed=1 parameter and loads embed template
	 */
	public static function handle_embed_request()
	{
		// Check for embed parameter.
		if (!isset($_GET['cha_embed']) || '1' !== $_GET['cha_embed']) {
			return;
		}

		// Load embed template.
		$template = plugin_dir_path(dirname(__FILE__)) . 'templates/embed-template.php';
		if (file_exists($template)) {
			include $template;
			exit;
		}
	}

	/**
	 * Render releases for embed
	 *
	 * @param int    $product_id Product ID.
	 * @param string $theme      Theme (light or dark).
	 */
	public static function render_embed_releases($product_id, $theme = 'light')
	{
		$args = [
			'limit' => 10,
			'order' => 'DESC',
			'product_id' => $product_id,
		];

		$query_args = [
			'post_type' => Changeloger_Post_Types::POST_TYPE,
			'posts_per_page' => $args['limit'],
			'post_status' => 'publish',
			'orderby' => 'meta_value',
			'meta_key' => Changeloger_Release_Meta::META_RELEASE_DATE,
			'order' => $args['order'],
		];

		if (!empty($args['product_id'])) {
			$query_args['tax_query'] = [
				[
					'taxonomy' => Changeloger_Post_Types::TAXONOMY_PRODUCT,
					'field' => 'term_id',
					'terms' => $args['product_id'],
				],
			];
		}

		$releases = new WP_Query($query_args);

		if (!$releases->have_posts()) {
			echo '<div class="cha-embed-empty"><p>' . esc_html__('No releases found.', 'changeloger') . '</p></div>';
			return;
		}

		$change_groups = Changeloger_Post_Types::get_change_groups();

		while ($releases->have_posts()) {
			$releases->the_post();
			$post_id = get_the_ID();
			$version = get_post_meta($post_id, Changeloger_Release_Meta::META_VERSION, true);
			$release_date = get_post_meta($post_id, Changeloger_Release_Meta::META_RELEASE_DATE, true);
			$change_items = get_post_meta($post_id, Changeloger_Release_Meta::META_CHANGE_ITEMS, true);
			$title = get_the_title($post_id);

			$formatted_date = '';
			if ($release_date) {
				$date_obj = DateTime::createFromFormat('Y-m-d', $release_date);
				if ($date_obj) {
					$formatted_date = $date_obj->format('j M, Y');
				}
			}
			// Get badge from post meta.
			$badge      = null;
			$badge_slug = get_post_meta( $post_id, '_cha_badge_slug', true );
			if ( ! empty( $badge_slug ) ) {
				$badges = Changeloger_Post_Types::get_release_badges();
				foreach ( $badges as $b ) {
					if ( $b['slug'] === $badge_slug ) {
						$badge = $b;
						break;
					}
				}
			}
			?>
			<div class="cha-release">
				<div class="cha-release-header">
					<span class="cha-release-version">v<?php echo esc_html($version ?: '0.0.0'); ?></span>
					<?php if ($badge): ?>
						<span class="cha-type-badge" style="background-color: <?php echo esc_attr($badge['color']); ?>20; color: <?php echo esc_attr($badge['color']); ?>"><?php echo esc_html($badge['name']); ?></span>
					<?php endif; ?>
					<span class="cha-release-title"><?php echo esc_html($title); ?></span>
					<?php if ($formatted_date): ?>
						<span class="cha-release-date"><?php echo esc_html($formatted_date); ?></span>
					<?php endif; ?>
				</div>

				<?php if (!empty($change_items) && is_array($change_items)): ?>
					<div class="cha-release-changes">
						<?php foreach ($change_items as $group): ?>
							<?php if (!empty($group['items'])): ?>
								<?php
								$group_label = isset($change_groups[$group['group']])
									? $change_groups[$group['group']]
									: ucfirst($group['group']);
								?>
								<div class="cha-change-group-label"><?php echo esc_html($group_label); ?></div>
								<?php foreach ($group['items'] as $item): ?>
									<div class="cha-change-item"><?php echo esc_html($item); ?></div>
								<?php endforeach; ?>
							<?php endif; ?>
						<?php endforeach; ?>
					</div>
				<?php endif; ?>
			</div>
			<?php
		}

		wp_reset_postdata();
	}
}
