<?php
/**
 * Right-pane Action Center — normalized item model and sanitization boundary.
 *
 * Separates eligibility checks, normalized item data, output escaping, and
 * rendering from the existing global notice system.
 *
 * @package SEOvaultSEO\Admin
 */

namespace SEOvaultSEO\Admin;

use SEOvaultSEO\Compatibility\Detector;
use SEOvaultSEO\Schema\SchemaMigration;
use SEOvaultSEO\Schema\SchemaOnboardingGuidance;

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

/**
 * Normalized, validated Action Center item model.
 *
 * Builds items from existing state providers, validates every value against
 * explicit allowlists, and produces safe normalized data for rendering.
 */
class ActionCenter {

	/* ── Item IDs ─────────────────────────────────────────────── */

	const ITEM_MIGRATION            = 'schema_migration';
	const ITEM_LEGACY_CONNECTOR     = 'legacy_connector';
	const ITEM_SCHEMA_ONBOARDING    = 'schema_onboarding';
	const ITEM_REVIEW               = 'review_request';
	const ITEM_COMPETITOR           = 'competitor';
	const ITEM_FEED_RECONCILIATION  = 'feed_reconciliation';
	const ITEM_SCHEMA_MONITORING    = 'schema_monitoring';

	/* ── Priorities (spec §4) ─────────────────────────────────── */

	const PRIORITY_MIGRATION         = 10;
	const PRIORITY_COMPETITOR        = 15;
	const PRIORITY_LEGACY_CONNECTOR  = 20;
	const PRIORITY_SCHEMA_ONBOARDING = 30;
	const PRIORITY_REVIEW            = 90;

	/* ── Severity values ──────────────────────────────────────── */

	const SEVERITY_INFO    = 'info';
	const SEVERITY_WARNING = 'warning';
	const SEVERITY_SUCCESS = 'success';

	/* ── Placements ───────────────────────────────────────────── */

	const PLACEMENT_SIDEBAR   = 'sidebar';
	const PLACEMENT_DASHBOARD = 'dashboard';

	/**
	 * Allowed placement values.
	 */
	const ALLOWED_PLACEMENTS = array(
		self::PLACEMENT_SIDEBAR,
		self::PLACEMENT_DASHBOARD,
	);

	/* ── Allowlists ───────────────────────────────────────────── */

	/**
	 * Allowed item IDs.
	 */
	const ALLOWED_IDS = array(
		self::ITEM_MIGRATION,
		self::ITEM_LEGACY_CONNECTOR,
		self::ITEM_SCHEMA_ONBOARDING,
		self::ITEM_REVIEW,
		self::ITEM_COMPETITOR,
		self::ITEM_FEED_RECONCILIATION,
		self::ITEM_SCHEMA_MONITORING,
	);

	/**
	 * Allowed severity values.
	 */
	const ALLOWED_SEVERITIES = array(
		self::SEVERITY_INFO,
		self::SEVERITY_WARNING,
		self::SEVERITY_SUCCESS,
	);

	/**
	 * Allowed Dashicon class names.
	 */
	const ALLOWED_ICONS = array(
		'dashicons-database',
		'dashicons-info',
		'dashicons-list-view',
		'dashicons-star-filled',
		'dashicons-warning',
		'dashicons-yes-alt',
		'dashicons-admin-plugins',
	);

	/**
	 * Allowed action identifiers (for future state-changing actions).
	 */
	const ALLOWED_ACTIONS = array(
		'review_migration',
		'dismiss_migration',
		'manage_plugins',
		'choose_identity',
		'dismiss_onboarding',
		'rate',
		'snooze',
		'dismiss',
		'snooze_review',
		'dismiss_review',
	);

	/* ── Public API ───────────────────────────────────────────── */

	/**
	 * Build normalized, validated items from existing state providers.
	 *
	 * @param string|null $placement Optional placement filter ('sidebar' or 'dashboard').
	 *                               When provided, only items with the matching placement
	 *                               are returned.  When null, all items are returned.
	 * @return array Normalized items sorted by priority (deterministic).
	 */
	public static function get_items( $placement = null ) {
		if ( null !== $placement && ! in_array( $placement, self::ALLOWED_PLACEMENTS, true ) ) {
			return array();
		}

		$items = array();

		if ( self::is_migration_eligible() ) {
			$items[] = self::build_migration_item();
		}

		if ( self::is_competitor_eligible() ) {
			$items[] = self::build_competitor_item();
		}

		if ( self::is_legacy_connector_eligible() ) {
			$items[] = self::build_legacy_connector_item();
		}

		if ( self::is_schema_onboarding_eligible() ) {
			$items[] = self::build_schema_onboarding_item();
		}

		if ( self::is_review_eligible() ) {
			$items[] = self::build_review_item();
		}

		/**
		 * Filter: seovaultseo_action_center_items
		 *
		 * Extensions may add or modify items.  Every item is re-validated
		 * after the filter runs; invalid items are silently omitted.
		 *
		 * @param array $items Raw item arrays.
		 */
		$items = apply_filters( 'seovaultseo_action_center_items', $items );

		$items = self::normalize_items( $items );

		if ( null !== $placement ) {
			$items = array_values(
				array_filter(
					$items,
					function ( $item ) use ( $placement ) {
						return isset( $item['placement'] ) && $placement === $item['placement'];
					}
				)
			);
		}

		return $items;
	}

	/* ── Eligibility checks (reuse existing state providers) ──── */

	/**
	 * Whether the schema migration Action Center item should appear.
	 *
	 * @return bool
	 */
	public static function is_migration_eligible() {
		if ( ! SchemaMigrationAdmin::current_user_can_manage() ) {
			return false;
		}
		return SchemaMigration::should_show_notice();
	}

	/**
	 * Whether the legacy connector Action Center item should appear.
	 *
	 * @return bool
	 */
	public static function is_legacy_connector_eligible() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return false;
		}
		return Detector::is_old_connector_active();
	}

	/**
	 * Whether the schema onboarding Action Center item should appear.
	 *
	 * @return bool
	 */
	public static function is_schema_onboarding_eligible() {
		return SchemaOnboardingGuidance::should_show_schema_notice();
	}

	/**
	 * Whether the review request Action Center item should appear.
	 *
	 * @return bool
	 */
	public static function is_review_eligible() {
		if ( ! Notices::is_review_eligible() ) {
			return false;
		}

		$user_id = get_current_user_id();
		if ( ! $user_id ) {
			return false;
		}

		$dismissed = get_user_meta( $user_id, 'seovaultseo_review_dismissed', true );
		if ( 'yes' === $dismissed ) {
			return false;
		}

		$snoozed = get_transient( 'seovaultseo_review_snooze_' . $user_id );
		if ( false !== $snoozed ) {
			return false;
		}

		return true;
	}

	/**
	 * Whether the competitor detection Action Center item should appear.
	 *
	 * This item is intended for Dashboard placement only. It surfaces
	 * when a competing SEO plugin is detected, regardless of output mode.
	 *
	 * @return bool
	 */
	public static function is_competitor_eligible() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return false;
		}
		return Detector::PROVIDER_NONE !== Detector::detect_active_provider();
	}

	/* ── Item builders (server-generated URLs only) ──────────── */

	/**
	 * Build the schema migration item.
	 *
	 * @return array
	 */
	private static function build_migration_item() {
		$dismiss_url = wp_nonce_url(
			admin_url( 'admin-post.php?action=' . SchemaMigrationAdmin::ACTION_DISMISS ),
			SchemaMigrationAdmin::NONCE_DISMISS
		);

		return array(
			'id'        => self::ITEM_MIGRATION,
			'priority'  => self::PRIORITY_MIGRATION,
			'placement' => self::PLACEMENT_SIDEBAR,
			'severity'  => self::SEVERITY_INFO,
			'icon'      => 'dashicons-database',
			'title'     => __( 'SEOvault schema update', 'seovault-ai' ),
			'summary'   => __( 'SEOvault updated your schema setup. Review the changes and your previous configuration backup.', 'seovault-ai' ),
			'primary'   => array(
				'label' => __( 'Review migration', 'seovault-ai' ),
				'url'   => SchemaMigrationAdmin::review_url(),
			),
			'secondary' => array(
				'label' => __( 'Dismiss', 'seovault-ai' ),
				'url'   => $dismiss_url,
			),
			'state'     => 'pending',
		);
	}

	/**
	 * Build the legacy connector item.
	 *
	 * @return array
	 */
	private static function build_legacy_connector_item() {
		return array(
			'id'        => self::ITEM_LEGACY_CONNECTOR,
			'priority'  => self::PRIORITY_LEGACY_CONNECTOR,
			'placement' => self::PLACEMENT_SIDEBAR,
			'severity'  => self::SEVERITY_INFO,
			'icon'      => 'dashicons-admin-plugins',
			'title'     => __( 'SEOvault AI Connector Detected', 'seovault-ai' ),
			'summary'   => __( 'The legacy connector plugin is still active. You can safely deactivate it — all functionality is now built in.', 'seovault-ai' ),
			'primary'   => array(
				'label' => __( 'Manage Plugins', 'seovault-ai' ),
				'url'   => admin_url( 'plugins.php' ),
			),
		);
	}

	/**
	 * Build the schema onboarding item.
	 *
	 * @return array
	 */
	private static function build_schema_onboarding_item() {
		$schema_url = admin_url( 'admin.php?page=seovault-ai-general&tab=vtab-schema' );
		$dismiss    = wp_nonce_url(
			admin_url( 'admin-post.php?action=' . SchemaOnboardingGuidance::ACTION_DISMISS ),
			SchemaOnboardingGuidance::NONCE_DISMISS
		);

		return array(
			'id'        => self::ITEM_SCHEMA_ONBOARDING,
			'priority'  => self::PRIORITY_SCHEMA_ONBOARDING,
			'placement' => self::PLACEMENT_SIDEBAR,
			'severity'  => self::SEVERITY_INFO,
			'icon'      => 'dashicons-list-view',
			'title'     => __( 'Confirm site identity for automatic schema', 'seovault-ai' ),
			'summary'   => SchemaOnboardingGuidance::automatic_basics_copy(),
			'primary'   => array(
				'label' => __( 'Choose Organization or Person', 'seovault-ai' ),
				'url'   => $schema_url,
			),
			'secondary' => array(
				'label' => __( 'Dismiss', 'seovault-ai' ),
				'url'   => $dismiss,
			),
		);
	}

	/**
	 * Build the review request item.
	 *
	 * @return array
	 */
	private static function build_review_item() {
		return array(
			'id'        => self::ITEM_REVIEW,
			'priority'  => self::PRIORITY_REVIEW,
			'placement' => self::PLACEMENT_SIDEBAR,
			'severity'  => self::SEVERITY_INFO,
			'icon'      => 'dashicons-star-filled',
			'title'     => __( 'Enjoying SEOvault AI?', 'seovault-ai' ),
			'summary'   => __( 'If SEOvault AI has been helpful, a 5-star review on WordPress.org helps others discover it.', 'seovault-ai' ),
			'primary'   => array(
				'label'    => __( 'Rate SEOvault AI', 'seovault-ai' ),
				'url'      => 'https://wordpress.org/support/plugin/seovault-ai-seo/reviews/#new-post',
				'action'   => 'rate',
				'external' => true,
			),
			'secondary' => array(
				'label'  => __( 'Maybe later', 'seovault-ai' ),
				'url'    => '#',
				'action' => 'snooze',
			),
			'tertiary'  => array(
				'label'  => __( 'I already did', 'seovault-ai' ),
				'url'    => '#',
				'action' => 'dismiss',
			),
		);
	}

	/**
	 * Build the competitor detection item.
	 *
	 * Intended for Dashboard placement. Mirrors the former inline
	 * Dashboard competitor notice but uses the unified Action Center
	 * data model, eligibility, and action URLs.
	 *
	 * @return array
	 */
	private static function build_competitor_item() {
		$provider = Detector::detect_active_provider();
		$name     = Detector::get_provider_label( $provider );

		return array(
			'id'        => self::ITEM_COMPETITOR,
			'priority'  => self::PRIORITY_COMPETITOR,
			'placement' => self::PLACEMENT_DASHBOARD,
			'severity'  => self::SEVERITY_WARNING,
			'icon'      => 'dashicons-warning',
			'title'     => sprintf(
				/* translators: %s: other SEO plugin name */
				__( '%s is also active', 'seovault-ai' ),
				$name
			),
			'summary'   => __( 'Import all needed SEO data while the other plugin is active. Then deactivate it to prevent duplicate output, or switch SEOvault AI to Disabled mode if you want to keep using it.', 'seovault-ai' ),
			'primary'   => array(
				'label' => __( 'Import SEO Data', 'seovault-ai' ),
				'url'   => admin_url( 'admin.php?page=seovault-ai-general&tab=vtab-tools' ),
			),
			'secondary' => array(
				'label' => __( 'Manage Plugins', 'seovault-ai' ),
				'url'   => admin_url( 'plugins.php' ),
			),
		);
	}

	/* ── Normalization and validation ─────────────────────────── */

	/**
	 * Normalize and validate a collection of items.
	 *
	 * Invalid items are silently omitted.  Duplicate IDs are deduplicated
	 * (first occurrence wins).  Items are sorted by priority ascending;
	 * equal-priority items preserve their original order (stable sort).
	 *
	 * @param mixed $items Raw item arrays (possibly from a filter).
	 * @return array Normalized items.
	 */
	public static function normalize_items( $items ) {
		if ( ! is_array( $items ) ) {
			return array();
		}

		$normalized = array();
		$seen_ids   = array();

		foreach ( $items as $item ) {
			$normalized_item = self::normalize_item( $item );
			if ( null === $normalized_item ) {
				continue;
			}

			if ( isset( $seen_ids[ $normalized_item['id'] ] ) ) {
				continue;
			}

			$seen_ids[ $normalized_item['id'] ] = true;
			$normalized[]                       = $normalized_item;
		}

		return self::stable_sort_by_priority( $normalized );
	}

	/**
	 * Normalize and validate a single item.
	 *
	 * Returns null if the item is invalid or missing required fields.
	 * Only allowlisted keys are retained in the output.
	 *
	 * @param mixed $item Raw item array.
	 * @return array|null Normalized item or null if invalid.
	 */
	public static function normalize_item( $item ) {
		if ( ! is_array( $item ) ) {
			return null;
		}

		$id = isset( $item['id'] ) ? self::sanitize_id( $item['id'] ) : '';
		if ( '' === $id ) {
			return null;
		}

		$priority = isset( $item['priority'] ) ? (int) $item['priority'] : 999;

		$severity = isset( $item['severity'] ) ? self::sanitize_severity( $item['severity'] ) : '';
		if ( '' === $severity ) {
			return null;
		}

		$icon      = isset( $item['icon'] ) ? self::sanitize_icon( $item['icon'] ) : '';
		$title     = isset( $item['title'] ) ? self::sanitize_text( $item['title'] ) : '';
		$summary   = isset( $item['summary'] ) ? self::sanitize_text( $item['summary'] ) : '';
		$state     = isset( $item['state'] ) ? self::sanitize_text( $item['state'] ) : '';
		$placement = isset( $item['placement'] ) ? self::sanitize_placement( $item['placement'] ) : self::PLACEMENT_SIDEBAR;

		if ( '' === $title ) {
			return null;
		}

		$primary = null;
		if ( isset( $item['primary'] ) && is_array( $item['primary'] ) ) {
			$primary = self::normalize_action( $item['primary'] );
		}

		$secondary = null;
		if ( isset( $item['secondary'] ) && is_array( $item['secondary'] ) ) {
			$secondary = self::normalize_action( $item['secondary'] );
		}

		$tertiary = null;
		if ( isset( $item['tertiary'] ) && is_array( $item['tertiary'] ) ) {
			$tertiary = self::normalize_action( $item['tertiary'] );
		}

		return array(
			'id'        => $id,
			'priority'  => $priority,
			'placement' => $placement,
			'severity'  => $severity,
			'icon'      => $icon,
			'title'     => $title,
			'summary'   => $summary,
			'primary'   => $primary,
			'secondary' => $secondary,
			'tertiary'  => $tertiary,
			'state'     => $state,
		);
	}

	/**
	 * Normalize a primary/secondary/tertiary action pair.
	 *
	 * @param mixed $action Raw action array.
	 * @return array|null Normalized action or null if invalid.
	 */
	private static function normalize_action( $action ) {
		if ( ! is_array( $action ) ) {
			return null;
		}

		$label = isset( $action['label'] ) ? self::sanitize_text( $action['label'] ) : '';
		$url   = isset( $action['url'] ) ? self::sanitize_url( $action['url'] ) : '';
		$act   = isset( $action['action'] ) ? self::sanitize_action( $action['action'] ) : '';
		$ext   = ! empty( $action['external'] ) && true === $action['external'];

		if ( '' === $label || '' === $url ) {
			return null;
		}

		return array(
			'label'    => $label,
			'url'      => $url,
			'action'   => $act,
			'external' => $ext,
		);
	}

	/* ── Sanitizers ───────────────────────────────────────────── */

	/**
	 * Validate an action identifier against the allowlist.
	 *
	 * @param mixed $value Raw value.
	 * @return string Valid action slug or empty string.
	 */
	public static function sanitize_action( $value ) {
		if ( ! is_string( $value ) ) {
			return '';
		}
		$value = sanitize_key( $value );
		if ( ! in_array( $value, self::ALLOWED_ACTIONS, true ) ) {
			return '';
		}
		return $value;
	}

	/**
	 * Validate an item ID against the allowlist.
	 *
	 * @param mixed $value Raw value.
	 * @return string Valid ID or empty string.
	 */
	public static function sanitize_id( $value ) {
		if ( ! is_string( $value ) ) {
			return '';
		}
		$value = sanitize_key( $value );
		if ( ! in_array( $value, self::ALLOWED_IDS, true ) ) {
			return '';
		}
		return $value;
	}

	/**
	 * Validate a placement value against the allowlist.
	 *
	 * Defaults to PLACEMENT_SIDEBAR when the value is invalid or missing.
	 *
	 * @param mixed $value Raw value.
	 * @return string Valid placement.
	 */
	public static function sanitize_placement( $value ) {
		if ( ! is_string( $value ) ) {
			return self::PLACEMENT_SIDEBAR;
		}
		$value = sanitize_key( $value );
		if ( ! in_array( $value, self::ALLOWED_PLACEMENTS, true ) ) {
			return self::PLACEMENT_SIDEBAR;
		}
		return $value;
	}

	/**
	 * Validate a severity value against the allowlist.
	 *
	 * @param mixed $value Raw value.
	 * @return string Valid severity or empty string.
	 */
	public static function sanitize_severity( $value ) {
		if ( ! is_string( $value ) ) {
			return '';
		}
		$value = sanitize_key( $value );
		if ( ! in_array( $value, self::ALLOWED_SEVERITIES, true ) ) {
			return '';
		}
		return $value;
	}

	/**
	 * Validate a Dashicon class against the allowlist.
	 *
	 * @param mixed $value Raw value.
	 * @return string Valid icon class or empty string.
	 */
	public static function sanitize_icon( $value ) {
		if ( ! is_string( $value ) ) {
			return '';
		}
		$value = trim( $value );
		if ( ! in_array( $value, self::ALLOWED_ICONS, true ) ) {
			return '';
		}
		return $value;
	}

	/**
	 * Sanitize a plain-text field (title, summary, label, state).
	 *
	 * Strips all HTML tags, normalizes whitespace, and trims.
	 *
	 * @param mixed $value Raw value.
	 * @return string Safe plain text.
	 */
	public static function sanitize_text( $value ) {
		if ( ! is_string( $value ) ) {
			return '';
		}
		$value = wp_strip_all_tags( $value, true );
		$value = preg_replace( '/\s+/u', ' ', $value );
		$value = trim( $value );
		return $value;
	}

	/**
	 * Validate and sanitize a URL.
	 *
	 * Only http and https protocols are accepted.  Protocol-relative URLs,
	 * javascript:, data:, vbscript:, malformed, and non-string values are
	 * rejected.
	 *
	 * @param mixed $value Raw URL value.
	 * @return string Safe URL or empty string if invalid.
	 */
	public static function sanitize_url( $value ) {
		if ( ! is_string( $value ) || '' === trim( $value ) ) {
			return '';
		}

		$value = trim( $value );

		// Allow '#' as a no-navigation placeholder for AJAX-driven actions.
		if ( '#' === $value ) {
			return '#';
		}

		// Reject protocol-relative URLs.
		if ( 0 === strpos( $value, '//' ) ) {
			return '';
		}

		// Reject URLs containing internal whitespace or control characters.
		if ( preg_match( '/[\x00-\x1f\s]/', $value ) ) {
			return '';
		}

		// Parse and validate the scheme.
		$scheme = wp_parse_url( $value, PHP_URL_SCHEME );
		if ( false === $scheme || null === $scheme ) {
			return '';
		}

		$scheme = strtolower( $scheme );
		if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
			return '';
		}

		// Use WordPress esc_url for additional validation.
		$clean = esc_url( $value );
		if ( '' === $clean ) {
			return '';
		}

		// Verify the scheme survived esc_url.
		$final_scheme = wp_parse_url( $clean, PHP_URL_SCHEME );
		if ( ! in_array( strtolower( (string) $final_scheme ), array( 'http', 'https' ), true ) ) {
			return '';
		}

		return $clean;
	}

	/* ── Sorting ──────────────────────────────────────────────── */

	/**
	 * Stable sort by priority ascending.
	 *
	 * PHP's usort is not stable before 8.0, so array_multisort is used
	 * with an index tiebreaker to preserve original order for equal
	 * priorities.
	 *
	 * @param array $items Normalized items.
	 * @return array Sorted items.
	 */
	private static function stable_sort_by_priority( $items ) {
		$count = count( $items );
		if ( $count <= 1 ) {
			return $items;
		}

		$priorities = array();
		$indices    = range( 0, $count - 1 );

		foreach ( $items as $item ) {
			$priorities[] = $item['priority'];
		}

		array_multisort(
			$priorities,
			SORT_ASC,
			SORT_NUMERIC,
			$indices,
			SORT_ASC,
			SORT_NUMERIC,
			$items
		);

		return $items;
	}

	/* ── Rendering ────────────────────────────────────────────── */

	/**
	 * Render the Action Center HTML.
	 *
	 * Returns an empty string when no items are eligible.
	 *
	 * @return string HTML markup or empty string.
	 */
	public static function render() {
		$items = self::get_items();
		if ( empty( $items ) ) {
			return '';
		}

		$buffer_level = ob_get_level();
		ob_start();
		try {
			?>
		<div class="seovaultseo-action-center" role="complementary" aria-label="<?php esc_attr_e( 'SEOvault Action Center', 'seovault-ai' ); ?>">
			<div class="seovaultseo-action-center__header">
				<h2><?php esc_html_e( 'Action Center', 'seovault-ai' ); ?></h2>
			</div>
			<ul class="seovaultseo-action-center__list">
				<?php foreach ( $items as $item ) : ?>
					<li class="seovaultseo-action-center__item seovaultseo-action-center__item--<?php echo esc_attr( $item['severity'] ); ?>" data-item-id="<?php echo esc_attr( $item['id'] ); ?>">
						<?php if ( '' !== $item['icon'] ) : ?>
							<?php if ( 'dashicons-star-filled' === $item['icon'] ) : ?>
								<span class="seovaultseo-action-center__icon seovaultseo-action-center__icon--star">
									<span class="dashicons <?php echo esc_attr( $item['icon'] ); ?>" aria-hidden="true"></span>
								</span>
							<?php else : ?>
								<span class="dashicons <?php echo esc_attr( $item['icon'] ); ?>" aria-hidden="true"></span>
							<?php endif; ?>
						<?php endif; ?>
						<div class="seovaultseo-action-center__content">
							<strong><?php echo esc_html( $item['title'] ); ?></strong>
							<?php if ( '' !== $item['summary'] ) : ?>
								<p><?php echo esc_html( $item['summary'] ); ?></p>
							<?php endif; ?>
							<div class="seovaultseo-action-center__actions">
								<?php if ( null !== $item['primary'] ) : ?>
									<a href="<?php echo esc_url( $item['primary']['url'] ); ?>" class="button button-primary button-small"<?php echo $item['primary']['external'] ? ' target="_blank" rel="noopener noreferrer"' : ''; ?><?php echo '' !== $item['primary']['action'] ? ' data-action="' . esc_attr( $item['primary']['action'] ) . '"' : ''; ?>>
										<?php echo esc_html( $item['primary']['label'] ); ?>
									</a>
								<?php endif; ?>
								<?php if ( null !== $item['secondary'] ) : ?>
									<a href="<?php echo esc_url( $item['secondary']['url'] ); ?>" class="button button-link"<?php echo $item['secondary']['external'] ? ' target="_blank" rel="noopener noreferrer"' : ''; ?><?php echo '' !== $item['secondary']['action'] ? ' data-action="' . esc_attr( $item['secondary']['action'] ) . '"' : ''; ?>>
										<?php echo esc_html( $item['secondary']['label'] ); ?>
									</a>
								<?php endif; ?>
								<?php if ( null !== $item['tertiary'] ) : ?>
									<a href="<?php echo esc_url( $item['tertiary']['url'] ); ?>" class="button button-link"<?php echo $item['tertiary']['external'] ? ' target="_blank" rel="noopener noreferrer"' : ''; ?><?php echo '' !== $item['tertiary']['action'] ? ' data-action="' . esc_attr( $item['tertiary']['action'] ) . '"' : ''; ?>>
										<?php echo esc_html( $item['tertiary']['label'] ); ?>
									</a>
								<?php endif; ?>
							</div>
						</div>
					</li>
				<?php endforeach; ?>
			</ul>
		</div>
			<?php
			return ob_get_clean();
		} finally {
			while ( ob_get_level() > $buffer_level ) {
				ob_end_clean();
			}
		}
	}

	/**
	 * Render dashboard-placement items using the dashboard visual wrapper.
	 *
	 * Returns an empty string when no dashboard items are eligible.
	 * Preserves the existing .seovaultseo-dashboard-notice CSS class
	 * for visual continuity with the former inline implementation.
	 *
	 * @return string HTML markup or empty string.
	 */
	public static function render_dashboard() {
		$items = self::get_items( self::PLACEMENT_DASHBOARD );
		if ( empty( $items ) ) {
			return '';
		}

		$buffer_level = ob_get_level();
		ob_start();
		try {
			?>
		<div class="seovaultseo-dashboard-notices">
			<?php foreach ( $items as $item ) : ?>
				<div class="seovaultseo-dashboard-notice" data-item-id="<?php echo esc_attr( $item['id'] ); ?>">
					<?php if ( '' !== $item['icon'] ) : ?>
						<span class="dashicons <?php echo esc_attr( $item['icon'] ); ?>"></span>
					<?php endif; ?>
					<p>
						<strong><?php echo esc_html( $item['title'] ); ?></strong>
						<?php if ( '' !== $item['summary'] ) : ?>
							<br><?php echo esc_html( $item['summary'] ); ?>
						<?php endif; ?>
					</p>
					<?php if ( null !== $item['primary'] ) : ?>
						<a href="<?php echo esc_url( $item['primary']['url'] ); ?>" class="button button-primary"<?php echo $item['primary']['external'] ? ' target="_blank" rel="noopener noreferrer"' : ''; ?><?php echo '' !== $item['primary']['action'] ? ' data-action="' . esc_attr( $item['primary']['action'] ) . '"' : ''; ?>>
							<?php echo esc_html( $item['primary']['label'] ); ?>
						</a>
					<?php endif; ?>
					<?php if ( null !== $item['secondary'] ) : ?>
						<a href="<?php echo esc_url( $item['secondary']['url'] ); ?>" class="button button-secondary"<?php echo $item['secondary']['external'] ? ' target="_blank" rel="noopener noreferrer"' : ''; ?><?php echo '' !== $item['secondary']['action'] ? ' data-action="' . esc_attr( $item['secondary']['action'] ) . '"' : ''; ?>>
							<?php echo esc_html( $item['secondary']['label'] ); ?>
						</a>
					<?php endif; ?>
					<?php if ( null !== $item['tertiary'] ) : ?>
						<a href="<?php echo esc_url( $item['tertiary']['url'] ); ?>" class="button button-link"<?php echo $item['tertiary']['external'] ? ' target="_blank" rel="noopener noreferrer"' : ''; ?><?php echo '' !== $item['tertiary']['action'] ? ' data-action="' . esc_attr( $item['tertiary']['action'] ) . '"' : ''; ?>>
							<?php echo esc_html( $item['tertiary']['label'] ); ?>
						</a>
					<?php endif; ?>
				</div>
			<?php endforeach; ?>
		</div>
			<?php
			return ob_get_clean();
		} finally {
			while ( ob_get_level() > $buffer_level ) {
				ob_end_clean();
			}
		}
	}
}
