<?php
/**
 * Options Manager
 *
 * @package SEOvaultSEO
 */

namespace SEOvaultSEO\Core;

/**
 * Handles plugin options
 */
class Options {

	// phpcs:disable Universal.NamingConventions.NoReservedKeywordParameterNames.defaultFound -- Backward-compatible public API.
	/**
	 * Get an option
	 *
	 * @param string $key Option key.
	 * @param mixed  $default Default value.
	 * @return mixed
	 */
	public static function get( $key, $default = '' ) {
		return get_option( 'seovaultseo_' . $key, $default );
	}
	// phpcs:enable Universal.NamingConventions.NoReservedKeywordParameterNames.defaultFound

	/**
	 * Set an option
	 *
	 * @param string $key Option key (without seovaultseo_ prefix).
	 * @param mixed  $value Option value.
	 * @param bool   $autoload Whether to autoload the option.
	 * @return bool
	 */
	public static function set( $key, $value, $autoload = false ) {
		return update_option( 'seovaultseo_' . $key, $value, $autoload );
	}

	/**
	 * Delete an option
	 *
	 * @param string $key Option key (without seovaultseo_ prefix).
	 * @return bool
	 */
	public static function delete( $key ) {
		return delete_option( 'seovaultseo_' . $key );
	}

	/**
	 * Get site key
	 *
	 * @return string
	 */
	public static function get_site_key() {
		return self::get( 'site_key', '' );
	}

	/**
	 * Set site key
	 *
	 * @param string $key Site key.
	 * @return bool
	 */
	public static function set_site_key( $key ) {
		return self::set( 'site_key', $key, false );
	}

	/**
	 * Get optional read-only API key (SEC-040). Empty until generated in admin.
	 *
	 * @return string
	 */
	public static function get_site_read_key() {
		return self::get( 'site_read_key', '' );
	}

	/**
	 * Set optional read-only API key.
	 *
	 * @param string $key Read key.
	 * @return bool
	 */
	public static function set_site_read_key( $key ) {
		return self::set( 'site_read_key', $key, false );
	}

	/**
	 * Get optional write-scoped API key (SEC-040). Empty until generated in admin.
	 *
	 * @return string
	 */
	public static function get_site_write_key() {
		return self::get( 'site_write_key', '' );
	}

	/**
	 * Set optional write-scoped API key.
	 *
	 * @param string $key Write key.
	 * @return bool
	 */
	public static function set_site_write_key( $key ) {
		return self::set( 'site_write_key', $key, false );
	}

	/**
	 * Get output mode
	 *
	 * @return string
	 */
	public static function get_output_mode() {
		return self::get( 'output_mode', 'auto' );
	}

	/**
	 * Set output mode
	 *
	 * @param string $mode Output mode (auto, manual, disabled).
	 * @return bool
	 */
	public static function set_output_mode( $mode ) {
		return self::set( 'output_mode', $mode );
	}

	/**
	 * Check if connection is verified
	 *
	 * @return bool
	 */
	public static function is_connection_verified() {
		return 'yes' === self::get( 'connection_verified', 'no' );
	}

	/**
	 * Set connection verified status
	 *
	 * @param string $status 'yes' or 'no'.
	 * @return bool
	 */
	public static function set_connection_verified( $status ) {
		return self::set( 'connection_verified', $status, false );
	}

	/**
	 * Get connected at timestamp
	 *
	 * @return string
	 */
	public static function get_connected_at() {
		return self::get( 'connected_at', '' );
	}

	/**
	 * Set connected at timestamp
	 *
	 * @param string $timestamp Timestamp.
	 * @return bool
	 */
	public static function set_connected_at( $timestamp ) {
		return self::set( 'connected_at', $timestamp, false );
	}

	// Homepage SEO options.

	/**
	 * Get homepage title
	 *
	 * @return string
	 */
	public static function get_home_title() {
		return self::get( 'home_title', '' );
	}

	/**
	 * Set homepage title
	 *
	 * @param string $title Homepage title.
	 * @return bool
	 */
	public static function set_home_title( $title ) {
		return self::set( 'home_title', $title );
	}

	/**
	 * Get homepage description
	 *
	 * @return string
	 */
	public static function get_home_description() {
		return self::get( 'home_description', '' );
	}

	/**
	 * Set homepage description
	 *
	 * @param string $description Homepage description.
	 * @return bool
	 */
	public static function set_home_description( $description ) {
		return self::set( 'home_description', $description );
	}

	// Template options.

	/**
	 * Get title template
	 *
	 * @return string
	 */
	public static function get_title_template() {
		$template = self::get( 'title_template', '' );
		return empty( $template ) ? '%title% %sep% %sitename%' : $template;
	}

	/**
	 * Set title template
	 *
	 * @param string $template Title template.
	 * @return bool
	 */
	public static function set_title_template( $template ) {
		return self::set( 'title_template', $template );
	}

	/**
	 * Get description template
	 *
	 * @return string
	 */
	public static function get_description_template() {
		$template = self::get( 'description_template', '' );
		return empty( $template ) ? '%excerpt%' : $template;
	}

	/**
	 * Set description template
	 *
	 * @param string $template Description template.
	 * @return bool
	 */
	public static function set_description_template( $template ) {
		return self::set( 'description_template', $template );
	}

	/**
	 * Get title separator.
	 *
	 * @return string
	 */
	public static function get_title_separator() {
		$sep = self::get( 'title_separator', '' );
		return empty( $sep ) ? '-' : $sep;
	}

	/**
	 * Set title separator.
	 *
	 * @param string $separator Separator character.
	 * @return bool
	 */
	public static function set_title_separator( $separator ) {
		return self::set( 'title_separator', $separator );
	}

	/**
	 * Check if templates are enabled.
	 *
	 * @return bool
	 */
	public static function is_templates_enabled() {
		return self::get( 'templates_enabled', 'yes' ) === 'yes';
	}

	/**
	 * Set templates enabled state.
	 *
	 * @param bool $enabled Whether templates are enabled.
	 * @return bool
	 */
	public static function set_templates_enabled( $enabled ) {
		return self::set( 'templates_enabled', $enabled ? 'yes' : 'no' );
	}

	// Social options.

	/**
	 * Get default social image
	 *
	 * @return string
	 */
	public static function get_default_social_image() {
		return self::get( 'default_social_image', '' );
	}

	/**
	 * Set default social image
	 *
	 * @param string $url Image URL.
	 * @return bool
	 */
	public static function set_default_social_image( $url ) {
		return self::set( 'default_social_image', $url );
	}

	/**
	 * Check if branding/additions should be removed from social titles.
	 *
	 * @return bool
	 */
	public static function is_social_title_rem_additions() {
		return (bool) self::get( 'social_title_rem_additions', false );
	}

	/**
	 * Set whether to remove branding/additions from social titles.
	 *
	 * @param bool $enabled Whether to remove additions.
	 * @return bool
	 */
	public static function set_social_title_rem_additions( $enabled ) {
		return self::set( 'social_title_rem_additions', $enabled ? '1' : '0' );
	}

	/**
	 * Get Facebook page URL.
	 *
	 * @return string
	 */
	public static function get_facebook_page_url() {
		return self::get( 'facebook_page_url', '' );
	}

	/**
	 * Set Facebook page URL.
	 *
	 * @param string $url Facebook page URL.
	 * @return bool
	 */
	public static function set_facebook_page_url( $url ) {
		return self::set( 'facebook_page_url', $url );
	}

	/**
	 * Get Facebook author fallback URL.
	 *
	 * @return string
	 */
	public static function get_facebook_author_fallback() {
		return self::get( 'facebook_author_fallback', '' );
	}

	/**
	 * Set Facebook author fallback URL.
	 *
	 * @param string $url Facebook author profile URL.
	 * @return bool
	 */
	public static function set_facebook_author_fallback( $url ) {
		return self::set( 'facebook_author_fallback', $url );
	}

	/**
	 * Get Twitter card type.
	 *
	 * @return string 'summary_large_image' or 'summary'.
	 */
	public static function get_twitter_card_type() {
		return self::get( 'twitter_card_type', 'summary_large_image' );
	}

	/**
	 * Set Twitter card type.
	 *
	 * @param string $type 'summary_large_image' or 'summary'.
	 * @return bool
	 */
	public static function set_twitter_card_type( $type ) {
		return self::set( 'twitter_card_type', $type );
	}

	/**
	 * Get Twitter/X profile URL.
	 *
	 * @return string
	 */
	public static function get_twitter_profile_username() {
		return self::get( 'twitter_profile_username', '' );
	}

	/**
	 * Set Twitter/X profile URL.
	 *
	 * @param string $url X profile URL.
	 * @return bool
	 */
	public static function set_twitter_profile_username( $url ) {
		return self::set( 'twitter_profile_username', $url );
	}

	/**
	 * Get Twitter/X author fallback profile URL.
	 *
	 * @return string
	 */
	public static function get_twitter_profile_fallback() {
		return self::get( 'twitter_profile_fallback', '' );
	}

	/**
	 * Set Twitter/X author fallback profile URL.
	 *
	 * @param string $url X author profile URL.
	 * @return bool
	 */
	public static function set_twitter_profile_fallback( $url ) {
		return self::set( 'twitter_profile_fallback', $url );
	}

	/**
	 * Get social profiles (array of URL strings).
	 *
	 * @return array
	 */
	public static function get_social_profiles() {
		$profiles = self::get( 'social_profiles', array() );
		if ( ! is_array( $profiles ) ) {
			return array();
		}
		return $profiles;
	}

	/**
	 * Set social profiles.
	 *
	 * @param array $profiles Array of URL strings.
	 * @return bool
	 */
	public static function set_social_profiles( $profiles ) {
		return self::set( 'social_profiles', is_array( $profiles ) ? $profiles : array() );
	}

	// Publishing defaults.

	/**
	 * Get default author ID
	 *
	 * @return int
	 */
	public static function get_default_author() {
		return (int) self::get( 'default_author', 0 );
	}

	/**
	 * Set default author ID
	 *
	 * @param int $author_id Author ID.
	 * @return bool
	 */
	public static function set_default_author( $author_id ) {
		return self::set( 'default_author', (int) $author_id );
	}

	/**
	 * Get default category ID
	 *
	 * @return int
	 */
	public static function get_default_category() {
		return (int) self::get( 'default_category', 0 );
	}

	/**
	 * Set default category ID
	 *
	 * @param int $category_id Category ID.
	 * @return bool
	 */
	public static function set_default_category( $category_id ) {
		return self::set( 'default_category', (int) $category_id );
	}

	// AI Visibility options.

	/**
	 * Get IndexNow key
	 *
	 * @return string
	 */
	public static function get_indexnow_key() {
		return self::get( 'indexnow_key', '' );
	}

	/**
	 * Set IndexNow key
	 *
	 * @param string $key IndexNow key.
	 * @return bool
	 */
	public static function set_indexnow_key( $key ) {
		return self::set( 'indexnow_key', $key, false );
	}

	/**
	 * Check if IndexNow auto-submit is enabled.
	 *
	 * @return bool
	 */
	public static function is_indexnow_enabled() {
		return (bool) self::get( 'indexnow_enabled', false );
	}

	/**
	 * Set IndexNow auto-submit enabled status.
	 *
	 * @param bool $enabled Whether auto-submit is enabled.
	 * @return bool
	 */
	public static function set_indexnow_enabled( $enabled ) {
		return self::set( 'indexnow_enabled', $enabled ? '1' : '0' );
	}

	/**
	 * Get post types for IndexNow auto-submit.
	 *
	 * @return array
	 */
	public static function get_indexnow_post_types() {
		$types = self::get( 'indexnow_post_types', array() );
		if ( ! is_array( $types ) ) {
			return array();
		}
		return $types;
	}

	/**
	 * Set post types for IndexNow auto-submit.
	 *
	 * @param array $types Array of post type slugs.
	 * @return bool
	 */
	public static function set_indexnow_post_types( $types ) {
		return self::set( 'indexnow_post_types', $types );
	}

	/**
	 * Get llms.txt content
	 *
	 * @return string
	 */
	public static function get_llms_txt_content() {
		return self::get( 'llms_txt_content', '' );
	}

	/**
	 * Set llms.txt content
	 *
	 * @param string $content llms.txt content.
	 * @return bool
	 */
	public static function set_llms_txt_content( $content ) {
		return self::set( 'llms_txt_content', $content, false );
	}

	/**
	 * Get llms-full.txt content
	 *
	 * @return string
	 */
	public static function get_llms_full_txt_content() {
		return self::get( 'llms_full_txt_content', '' );
	}

	/**
	 * Set llms-full.txt content
	 *
	 * @param string $content llms-full.txt content.
	 * @return bool
	 */
	public static function set_llms_full_txt_content( $content ) {
		return self::set( 'llms_full_txt_content', $content, false );
	}

	/**
	 * Get llms served by
	 *
	 * @return string
	 */
	public static function get_llms_served_by() {
		return self::get( 'llms_served_by', 'unknown' );
	}

	/**
	 * Set llms served by
	 *
	 * @param string $source Source identifier.
	 * @return bool
	 */
	public static function set_llms_served_by( $source ) {
		return self::set( 'llms_served_by', $source );
	}

	/**
	 * Check if llms is enabled
	 *
	 * @return bool
	 */
	public static function is_llms_enabled() {
		return 'yes' === self::get( 'llms_enabled', 'no' );
	}

	/**
	 * Set llms enabled status
	 *
	 * @param string $status 'yes' or 'no'.
	 * @return bool
	 */
	public static function set_llms_enabled( $status ) {
		return self::set( 'llms_enabled', $status );
	}

	/**
	 * Get robots policy
	 *
	 * @return string
	 */
	public static function get_robots_policy() {
		return self::get( 'robots_policy', '' );
	}

	/**
	 * Set robots policy
	 *
	 * @param string $policy Policy name.
	 * @return bool
	 */
	public static function set_robots_policy( $policy ) {
		return self::set( 'robots_policy', $policy );
	}

	/**
	 * Get robots custom content
	 *
	 * @return string
	 */
	public static function get_robots_custom_content() {
		return self::get( 'robots_custom_content', '' );
	}

	/**
	 * Set robots custom content
	 *
	 * @param string $content Custom robots.txt content.
	 * @return bool
	 */
	public static function set_robots_custom_content( $content ) {
		return self::set( 'robots_custom_content', $content, false );
	}

	/**
	 * Get crawler logs
	 *
	 * @return array
	 */
	public static function get_crawler_logs() {
		$logs = self::get( 'crawler_logs', '[]' );
		return is_string( $logs ) ? json_decode( $logs, true ) : array();
	}

	/**
	 * Set crawler logs
	 *
	 * @param array $logs Crawler logs.
	 * @return bool
	 */
	public static function set_crawler_logs( $logs ) {
		return self::set( 'crawler_logs', wp_json_encode( $logs ), false );
	}

	/**
	 * Check if crawler logging is enabled
	 *
	 * @return bool
	 */
	public static function is_crawler_logging_enabled() {
		return (bool) self::get( 'crawler_logging_enabled', false );
	}

	/**
	 * Set crawler logging enabled status
	 *
	 * @param bool $enabled Whether logging is enabled.
	 * @return bool
	 */
	public static function set_crawler_logging_enabled( $enabled ) {
		return self::set( 'crawler_logging_enabled', $enabled ? '1' : '0' );
	}

	// Breadcrumbs options.

	/**
	 * Check if breadcrumbs are enabled.
	 *
	 * @return bool
	 */
	public static function is_breadcrumbs_enabled() {
		return (bool) self::get( 'breadcrumbs_enabled', true );
	}

	/**
	 * Set breadcrumbs enabled status.
	 *
	 * @param bool $enabled Whether breadcrumbs are enabled.
	 * @return bool
	 */
	public static function set_breadcrumbs_enabled( $enabled ) {
		return self::set( 'breadcrumbs_enabled', $enabled ? '1' : '0' );
	}

	/**
	 * Get breadcrumbs separator.
	 *
	 * @return string
	 */
	public static function get_breadcrumbs_separator() {
		return self::get( 'breadcrumbs_separator', '&raquo;' );
	}

	/**
	 * Set breadcrumbs separator.
	 *
	 * @param string $separator Separator character or string.
	 * @return bool
	 */
	public static function set_breadcrumbs_separator( $separator ) {
		return self::set( 'breadcrumbs_separator', $separator );
	}

	/**
	 * Get breadcrumbs home label.
	 *
	 * @return string
	 */
	public static function get_breadcrumbs_home_label() {
		return self::get( 'breadcrumbs_home_label', __( 'Home', 'seovault-ai' ) );
	}

	/**
	 * Set breadcrumbs home label.
	 *
	 * @param string $label Home label text.
	 * @return bool
	 */
	public static function set_breadcrumbs_home_label( $label ) {
		return self::set( 'breadcrumbs_home_label', $label );
	}

	/**
	 * Get breadcrumbs prefix.
	 *
	 * @return string
	 */
	public static function get_breadcrumbs_prefix() {
		return self::get( 'breadcrumbs_prefix', '' );
	}

	/**
	 * Set breadcrumbs prefix.
	 *
	 * @param string $prefix Prefix text.
	 * @return bool
	 */
	public static function set_breadcrumbs_prefix( $prefix ) {
		return self::set( 'breadcrumbs_prefix', $prefix );
	}

	/**
	 * Get breadcrumbs 404 label.
	 *
	 * @return string
	 */
	public static function get_breadcrumbs_404_label() {
		return self::get( 'breadcrumbs_404_label', __( '404 Error: page not found', 'seovault-ai' ) );
	}

	/**
	 * Set breadcrumbs 404 label.
	 *
	 * @param string $label 404 label text.
	 * @return bool
	 */
	public static function set_breadcrumbs_404_label( $label ) {
		return self::set( 'breadcrumbs_404_label', $label );
	}

	/**
	 * Get breadcrumbs search format.
	 *
	 * @return string
	 */
	public static function get_breadcrumbs_search_format() {
		/* translators: %s: search query text. */
		return self::get( 'breadcrumbs_search_format', __( 'Results for %s', 'seovault-ai' ) );
	}

	/**
	 * Set breadcrumbs search format.
	 *
	 * @param string $format Search format string.
	 * @return bool
	 */
	public static function set_breadcrumbs_search_format( $format ) {
		return self::set( 'breadcrumbs_search_format', $format );
	}

	/**
	 * Get breadcrumbs archive format.
	 *
	 * @return string
	 */
	public static function get_breadcrumbs_archive_format() {
		/* translators: %s: archive title. */
		return self::get( 'breadcrumbs_archive_format', __( 'Archives for %s', 'seovault-ai' ) );
	}

	/**
	 * Set breadcrumbs archive format.
	 *
	 * @param string $format Archive format string.
	 * @return bool
	 */
	public static function set_breadcrumbs_archive_format( $format ) {
		return self::set( 'breadcrumbs_archive_format', $format );
	}

	/**
	 * Check if breadcrumbs should show homepage link.
	 *
	 * @return bool
	 */
	public static function breadcrumbs_show_home() {
		return (bool) self::get( 'breadcrumbs_show_home', true );
	}

	/**
	 * Set breadcrumbs show homepage link.
	 *
	 * @param bool $show Whether to show homepage link.
	 * @return bool
	 */
	public static function set_breadcrumbs_show_home( $show ) {
		return self::set( 'breadcrumbs_show_home', $show ? '1' : '0' );
	}

	/**
	 * Check if breadcrumbs should remove post title.
	 *
	 * @return bool
	 */
	public static function breadcrumbs_remove_post_title() {
		return (bool) self::get( 'breadcrumbs_remove_post_title', false );
	}

	/**
	 * Set breadcrumbs remove post title.
	 *
	 * @param bool $remove Whether to remove post title.
	 * @return bool
	 */
	public static function set_breadcrumbs_remove_post_title( $remove ) {
		return self::set( 'breadcrumbs_remove_post_title', $remove ? '1' : '0' );
	}

	/**
	 * Check if breadcrumbs should show ancestor categories.
	 *
	 * @return bool
	 */
	public static function breadcrumbs_show_ancestors() {
		return (bool) self::get( 'breadcrumbs_show_ancestors', false );
	}

	/**
	 * Set breadcrumbs show ancestor categories.
	 *
	 * @param bool $show Whether to show ancestors.
	 * @return bool
	 */
	public static function set_breadcrumbs_show_ancestors( $show ) {
		return self::set( 'breadcrumbs_show_ancestors', $show ? '1' : '0' );
	}

	/**
	 * Check if breadcrumbs should hide taxonomy name.
	 *
	 * @return bool
	 */
	public static function breadcrumbs_hide_taxonomy_name() {
		return (bool) self::get( 'breadcrumbs_hide_taxonomy_name', false );
	}

	/**
	 * Set breadcrumbs hide taxonomy name.
	 *
	 * @param bool $hide Whether to hide taxonomy name.
	 * @return bool
	 */
	public static function set_breadcrumbs_hide_taxonomy_name( $hide ) {
		return self::set( 'breadcrumbs_hide_taxonomy_name', $hide ? '1' : '0' );
	}

	/**
	 * Check if breadcrumbs should show blog page.
	 *
	 * @return bool
	 */
	public static function breadcrumbs_show_blog() {
		return (bool) self::get( 'breadcrumbs_show_blog', false );
	}

	/**
	 * Set breadcrumbs show blog page.
	 *
	 * @param bool $show Whether to show blog page.
	 * @return bool
	 */
	public static function set_breadcrumbs_show_blog( $show ) {
		return self::set( 'breadcrumbs_show_blog', $show ? '1' : '0' );
	}

	// 404 Monitor options.

	/**
	 * Check if 404 monitor is enabled.
	 *
	 * @return bool
	 */
	public static function is_monitor404_enabled() {
		return (bool) self::get( 'monitor404_enabled', false );
	}

	/**
	 * Set 404 monitor enabled status.
	 *
	 * @param bool $enabled Whether 404 monitor is enabled.
	 * @return bool
	 */
	public static function set_monitor404_enabled( $enabled ) {
		return self::set( 'monitor404_enabled', $enabled ? '1' : '0' );
	}

	/**
	 * Get 404 monitor mode: 'simple' or 'advanced'.
	 *
	 * @return string
	 */
	public static function get_monitor404_mode() {
		return self::get( 'monitor404_mode', 'simple' );
	}

	/**
	 * Set 404 monitor mode.
	 *
	 * @param string $mode 'simple' or 'advanced'.
	 * @return bool
	 */
	public static function set_monitor404_mode( $mode ) {
		return self::set( 'monitor404_mode', $mode );
	}

	/**
	 * Get 404 monitor log limit (max entries before auto-cleanup).
	 *
	 * @return int 0 = no limit.
	 */
	public static function get_monitor404_limit() {
		return (int) self::get( 'monitor404_limit', 1000 );
	}

	/**
	 * Set 404 monitor log limit.
	 *
	 * @param int $limit Max entries (0 = no limit).
	 * @return bool
	 */
	public static function set_monitor404_limit( $limit ) {
		return self::set( 'monitor404_limit', (int) $limit );
	}

	/**
	 * Check if query parameters should be ignored when logging 404s.
	 *
	 * @return bool
	 */
	public static function is_monitor404_ignore_query_params() {
		return (bool) self::get( 'monitor404_ignore_query_params', false );
	}

	/**
	 * Set whether to ignore query parameters.
	 *
	 * @param bool $ignore Whether to ignore query params.
	 * @return bool
	 */
	public static function set_monitor404_ignore_query_params( $ignore ) {
		return self::set( 'monitor404_ignore_query_params', $ignore ? '1' : '0' );
	}

	/**
	 * Get 404 monitor exclusion patterns.
	 *
	 * @return array Array of { pattern: string, comparison: string }.
	 */
	public static function get_monitor404_excludes() {
		$excludes = self::get( 'monitor404_excludes', array() );
		if ( ! is_array( $excludes ) ) {
			return array();
		}
		return $excludes;
	}

	/**
	 * Set 404 monitor exclusion patterns.
	 *
	 * @param array $excludes Array of exclusion rules.
	 * @return bool
	 */
	public static function set_monitor404_excludes( $excludes ) {
		return self::set( 'monitor404_excludes', $excludes );
	}

	// NotFound fallback options (Layer A).
	// Setters sanitize via NotFound\Sanitizer.

	/**
	 * Get 404 fallback mode: theme | home | custom.
	 *
	 * @return string
	 */
	public static function get_notfound_fallback_mode() {
		return \SEOvaultSEO\NotFound\Sanitizer::sanitize_fallback_mode(
			self::get( 'notfound_fallback_mode', 'theme' )
		);
	}

	/**
	 * Set 404 fallback mode.
	 *
	 * @param string $mode theme | home | custom.
	 * @return bool
	 */
	public static function set_notfound_fallback_mode( $mode ) {
		return self::set(
			'notfound_fallback_mode',
			\SEOvaultSEO\NotFound\Sanitizer::sanitize_fallback_mode( $mode )
		);
	}

	/**
	 * Get 404 fallback redirect status code.
	 *
	 * @return int 301, 302, or 307 (default 302).
	 */
	public static function get_notfound_fallback_status() {
		return \SEOvaultSEO\NotFound\Sanitizer::sanitize_fallback_status(
			self::get( 'notfound_fallback_status', 302 )
		);
	}

	/**
	 * Set 404 fallback redirect status code.
	 *
	 * @param int|string $status 301 | 302 | 307.
	 * @return bool
	 */
	public static function set_notfound_fallback_status( $status ) {
		return self::set(
			'notfound_fallback_status',
			\SEOvaultSEO\NotFound\Sanitizer::sanitize_fallback_status( $status )
		);
	}

	/**
	 * Get custom fallback URL.
	 *
	 * @return string
	 */
	public static function get_notfound_fallback_custom_url() {
		$url = self::get( 'notfound_fallback_custom_url', '' );
		return is_string( $url ) ? $url : '';
	}

	/**
	 * Set custom fallback URL.
	 *
	 * @param string $url Absolute URL.
	 * @return bool
	 */
	public static function set_notfound_fallback_custom_url( $url ) {
		return self::set(
			'notfound_fallback_custom_url',
			\SEOvaultSEO\NotFound\Sanitizer::sanitize_custom_url( $url )
		);
	}

	/**
	 * Get custom fallback page ID.
	 *
	 * @return int
	 */
	public static function get_notfound_fallback_custom_page_id() {
		return (int) self::get( 'notfound_fallback_custom_page_id', 0 );
	}

	/**
	 * Set custom fallback page ID.
	 *
	 * @param int $page_id Published page ID.
	 * @return bool
	 */
	public static function set_notfound_fallback_custom_page_id( $page_id ) {
		return self::set(
			'notfound_fallback_custom_page_id',
			\SEOvaultSEO\NotFound\Sanitizer::sanitize_custom_page_id( $page_id )
		);
	}

	/**
	 * Check whether redirected 404 fallbacks should be logged.
	 *
	 * @return bool
	 */
	public static function is_notfound_log_redirected_fallbacks() {
		return \SEOvaultSEO\NotFound\Sanitizer::sanitize_boolean(
			self::get( 'notfound_log_redirected_fallbacks', true )
		);
	}

	/**
	 * Set whether redirected 404 fallbacks should be logged.
	 *
	 * @param mixed $enabled Whether redirected fallbacks should be logged.
	 * @return bool
	 */
	public static function set_notfound_log_redirected_fallbacks( $enabled ) {
		$enabled = \SEOvaultSEO\NotFound\Sanitizer::sanitize_boolean( $enabled );
		return self::set( 'notfound_log_redirected_fallbacks', $enabled ? '1' : '0' );
	}

	// NotFound recovery options (Layer B).
	// Setters and getters sanitize via NotFound\Sanitizer.

	/**
	 * Check whether the built-in 404 recovery page is enabled.
	 *
	 * @return bool
	 */
	public static function is_notfound_recovery_enabled() {
		return \SEOvaultSEO\NotFound\Sanitizer::sanitize_boolean(
			self::get( 'notfound_recovery_enabled', false )
		);
	}

	/**
	 * Set whether the built-in 404 recovery page is enabled.
	 *
	 * @param bool $enabled Whether recovery is enabled.
	 * @return bool
	 */
	public static function set_notfound_recovery_enabled( $enabled ) {
		$enabled = \SEOvaultSEO\NotFound\Sanitizer::sanitize_boolean( $enabled );
		return self::set( 'notfound_recovery_enabled', $enabled ? '1' : '0' );
	}

	/**
	 * Get the recovery page headline.
	 *
	 * @return string
	 */
	public static function get_notfound_recovery_headline() {
		$headline = \SEOvaultSEO\NotFound\Sanitizer::sanitize_recovery_text(
			self::get( 'notfound_recovery_headline', '' )
		);
		return '' !== $headline
			? $headline
			: __( 'Page not found', 'seovault-ai' );
	}

	/**
	 * Set the recovery page headline.
	 *
	 * @param string $headline Recovery headline.
	 * @return bool
	 */
	public static function set_notfound_recovery_headline( $headline ) {
		return self::set(
			'notfound_recovery_headline',
			\SEOvaultSEO\NotFound\Sanitizer::sanitize_recovery_text( $headline )
		);
	}

	/**
	 * Get the recovery page message.
	 *
	 * @return string
	 */
	public static function get_notfound_recovery_message() {
		$message = \SEOvaultSEO\NotFound\Sanitizer::sanitize_recovery_text(
			self::get( 'notfound_recovery_message', '' )
		);
		return '' !== $message
			? $message
			: __( 'We can\'t find that page. Try searching the site or return home.', 'seovault-ai' );
	}

	/**
	 * Set the recovery page message.
	 *
	 * @param string $message Recovery message.
	 * @return bool
	 */
	public static function set_notfound_recovery_message( $message ) {
		return self::set(
			'notfound_recovery_message',
			\SEOvaultSEO\NotFound\Sanitizer::sanitize_recovery_text( $message )
		);
	}

	/**
	 * Check whether the recovery page shows a search form.
	 *
	 * @return bool
	 */
	public static function is_notfound_recovery_show_search() {
		return \SEOvaultSEO\NotFound\Sanitizer::sanitize_boolean(
			self::get( 'notfound_recovery_show_search', true )
		);
	}

	/**
	 * Set whether the recovery page shows a search form.
	 *
	 * @param bool $show Whether to show search.
	 * @return bool
	 */
	public static function set_notfound_recovery_show_search( $show ) {
		$show = \SEOvaultSEO\NotFound\Sanitizer::sanitize_boolean( $show );
		return self::set( 'notfound_recovery_show_search', $show ? '1' : '0' );
	}

	/**
	 * Check whether the recovery page shows the home call to action.
	 *
	 * @return bool
	 */
	public static function is_notfound_recovery_show_home_cta() {
		return \SEOvaultSEO\NotFound\Sanitizer::sanitize_boolean(
			self::get( 'notfound_recovery_show_home_cta', true )
		);
	}

	/**
	 * Set whether the recovery page shows the home call to action.
	 *
	 * @param bool $show Whether to show the home call to action.
	 * @return bool
	 */
	public static function set_notfound_recovery_show_home_cta( $show ) {
		$show = \SEOvaultSEO\NotFound\Sanitizer::sanitize_boolean( $show );
		return self::set( 'notfound_recovery_show_home_cta', $show ? '1' : '0' );
	}

	/**
	 * Get the recovery call-to-action label.
	 *
	 * @return string
	 */
	public static function get_notfound_recovery_cta_label() {
		$label = \SEOvaultSEO\NotFound\Sanitizer::sanitize_recovery_text(
			self::get( 'notfound_recovery_cta_label', '' )
		);
		return '' !== $label
			? $label
			: __( 'Go to homepage', 'seovault-ai' );
	}

	/**
	 * Set the recovery call-to-action label.
	 *
	 * @param string $label Call-to-action label.
	 * @return bool
	 */
	public static function set_notfound_recovery_cta_label( $label ) {
		return self::set(
			'notfound_recovery_cta_label',
			\SEOvaultSEO\NotFound\Sanitizer::sanitize_recovery_text( $label )
		);
	}

	/**
	 * Get the recovery call-to-action URL.
	 *
	 * @return string
	 */
	public static function get_notfound_recovery_cta_url() {
		return \SEOvaultSEO\NotFound\Sanitizer::sanitize_recovery_cta_url(
			self::get( 'notfound_recovery_cta_url', '' )
		);
	}

	/**
	 * Set the recovery call-to-action URL.
	 *
	 * @param string $url Call-to-action URL. Empty uses the homepage.
	 * @return bool
	 */
	public static function set_notfound_recovery_cta_url( $url ) {
		return self::set(
			'notfound_recovery_cta_url',
			\SEOvaultSEO\NotFound\Sanitizer::sanitize_recovery_cta_url( $url )
		);
	}

	/**
	 * Check whether the recovery page shows recent posts.
	 *
	 * @return bool
	 */
	public static function is_notfound_recovery_show_recent() {
		return \SEOvaultSEO\NotFound\Sanitizer::sanitize_boolean(
			self::get( 'notfound_recovery_show_recent', true )
		);
	}

	/**
	 * Set whether the recovery page shows recent posts.
	 *
	 * @param bool $show Whether to show recent posts.
	 * @return bool
	 */
	public static function set_notfound_recovery_show_recent( $show ) {
		$show = \SEOvaultSEO\NotFound\Sanitizer::sanitize_boolean( $show );
		return self::set( 'notfound_recovery_show_recent', $show ? '1' : '0' );
	}

	/**
	 * Get the number of recent posts shown on the recovery page.
	 *
	 * @return int
	 */
	public static function get_notfound_recovery_recent_count() {
		return \SEOvaultSEO\NotFound\Sanitizer::sanitize_recovery_recent_count(
			self::get( 'notfound_recovery_recent_count', 3 )
		);
	}

	/**
	 * Set the number of recent posts shown on the recovery page.
	 *
	 * @param int $count Recent post count.
	 * @return bool
	 */
	public static function set_notfound_recovery_recent_count( $count ) {
		return self::set(
			'notfound_recovery_recent_count',
			\SEOvaultSEO\NotFound\Sanitizer::sanitize_recovery_recent_count( $count )
		);
	}

	/**
	 * Check whether recovery styling should use theme colors.
	 *
	 * @return bool
	 */
	public static function is_notfound_recovery_use_theme_colors() {
		return \SEOvaultSEO\NotFound\Sanitizer::sanitize_boolean(
			self::get( 'notfound_recovery_use_theme_colors', true )
		);
	}

	/**
	 * Set whether recovery styling should use theme colors.
	 *
	 * @param bool $use_theme_colors Whether to use theme colors.
	 * @return bool
	 */
	public static function set_notfound_recovery_use_theme_colors( $use_theme_colors ) {
		$use_theme_colors = \SEOvaultSEO\NotFound\Sanitizer::sanitize_boolean( $use_theme_colors );
		return self::set( 'notfound_recovery_use_theme_colors', $use_theme_colors ? '1' : '0' );
	}

	/**
	 * Get the recovery accent color.
	 *
	 * @return string
	 */
	public static function get_notfound_recovery_accent() {
		return \SEOvaultSEO\NotFound\Sanitizer::sanitize_recovery_accent(
			self::get( 'notfound_recovery_accent', '#1a1a1a' )
		);
	}

	/**
	 * Set the recovery accent color.
	 *
	 * @param string $accent Accent color.
	 * @return bool
	 */
	public static function set_notfound_recovery_accent( $accent ) {
		return self::set(
			'notfound_recovery_accent',
			\SEOvaultSEO\NotFound\Sanitizer::sanitize_recovery_accent( $accent )
		);
	}

	/**
	 * Get the recovery content mode.
	 *
	 * @return string
	 */
	public static function get_notfound_recovery_content_mode() {
		return \SEOvaultSEO\NotFound\Sanitizer::sanitize_recovery_content_mode(
			self::get( 'notfound_recovery_content_mode', 'builtin' )
		);
	}

	/**
	 * Set the recovery content mode.
	 *
	 * @param string $mode Recovery content mode.
	 * @return bool
	 */
	public static function set_notfound_recovery_content_mode( $mode ) {
		return self::set(
			'notfound_recovery_content_mode',
			\SEOvaultSEO\NotFound\Sanitizer::sanitize_recovery_content_mode( $mode )
		);
	}

	/**
	 * Get the recovery content page ID.
	 *
	 * Returns the stored ID without re-checking publish status so a page that
	 * later becomes draft/private can fall back at render time. Save-time
	 * sanitization still rejects non-published pages.
	 *
	 * @return int
	 */
	public static function get_notfound_recovery_page_id() {
		return absint( self::get( 'notfound_recovery_page_id', 0 ) );
	}

	/**
	 * Set the recovery content page ID.
	 *
	 * @param int $page_id Recovery page ID.
	 * @return bool
	 */
	public static function set_notfound_recovery_page_id( $page_id ) {
		return self::set(
			'notfound_recovery_page_id',
			\SEOvaultSEO\NotFound\Sanitizer::sanitize_recovery_page_id( $page_id )
		);
	}

	// Image SEO options.

	/**
	 * Check if Image SEO is enabled.
	 *
	 * @return bool
	 */
	public static function is_image_seo_enabled() {
		return (bool) self::get( 'image_seo_enabled', false );
	}

	/**
	 * Set Image SEO enabled status.
	 *
	 * @param bool $enabled Whether Image SEO is enabled.
	 * @return bool
	 */
	public static function set_image_seo_enabled( $enabled ) {
		return self::set( 'image_seo_enabled', $enabled ? '1' : '0' );
	}

	/**
	 * Get the ALT attribute format string.
	 *
	 * @return string|false Returns false if not set.
	 */
	public static function get_image_seo_alt_format() {
		$format = self::get( 'image_seo_alt_format', '' );
		return '' !== trim( $format ) ? trim( $format ) : false;
	}

	/**
	 * Set the ALT attribute format string.
	 *
	 * @param string $format Format string (e.g. "%title%").
	 * @return bool
	 */
	public static function set_image_seo_alt_format( $format ) {
		return self::set( 'image_seo_alt_format', $format );
	}

	/**
	 * Get the title attribute format string.
	 *
	 * @return string|false Returns false if not set.
	 */
	public static function get_image_seo_title_format() {
		$format = self::get( 'image_seo_title_format', '' );
		return '' !== trim( $format ) ? trim( $format ) : false;
	}

	/**
	 * Set the title attribute format string.
	 *
	 * @param string $format Format string (e.g. "%title%").
	 * @return bool
	 */
	public static function set_image_seo_title_format( $format ) {
		return self::set( 'image_seo_title_format', $format );
	}

	/**
	 * Check if existing empty ALT attributes should be overridden.
	 *
	 * @return bool
	 */
	public static function is_image_seo_override_empty_alt() {
		return (bool) self::get( 'image_seo_override_empty_alt', true );
	}

	/**
	 * Set whether to override existing empty ALT attributes.
	 *
	 * @param bool $override Whether to override.
	 * @return bool
	 */
	public static function set_image_seo_override_empty_alt( $override ) {
		return self::set( 'image_seo_override_empty_alt', $override ? '1' : '0' );
	}

	/**
	 * Check if web app image data should take priority over auto-generated ALT.
	 *
	 * @return bool
	 */
	public static function is_image_seo_webapp_priority() {
		return (bool) self::get( 'image_seo_webapp_priority', true );
	}

	// Frontend SEO Score options.

	/**
	 * Check if frontend SEO score display is enabled.
	 *
	 * @return bool
	 */
	public static function is_frontend_score_enabled() {
		return (bool) self::get( 'frontend_score_enabled', false );
	}

	/**
	 * Set frontend SEO score enabled status.
	 *
	 * @param bool $enabled Whether frontend score display is enabled.
	 * @return bool
	 */
	public static function set_frontend_score_enabled( $enabled ) {
		return self::set( 'frontend_score_enabled', $enabled ? '1' : '0' );
	}

	/**
	 * Get frontend score position (top, bottom, both, custom).
	 *
	 * @return string
	 */
	public static function get_frontend_score_position() {
		return self::get( 'frontend_score_position', 'bottom' );
	}

	/**
	 * Set frontend score position.
	 *
	 * @param string $position Position: top, bottom, both, or custom.
	 * @return bool
	 */
	public static function set_frontend_score_position( $position ) {
		return self::set( 'frontend_score_position', $position );
	}

	/**
	 * Get post types where frontend score should be displayed.
	 *
	 * @return array
	 */
	public static function get_frontend_score_post_types() {
		$types = self::get( 'frontend_score_post_types', array( 'post' ) );
		if ( ! is_array( $types ) ) {
			return array( 'post' );
		}
		return $types;
	}

	/**
	 * Set frontend score post types.
	 *
	 * @param array $types Array of post type slugs.
	 * @return bool
	 */
	public static function set_frontend_score_post_types( $types ) {
		return self::set( 'frontend_score_post_types', $types );
	}

	/**
	 * Set whether web app image data takes priority.
	 *
	 * @param bool $priority Whether web app data takes priority.
	 * @return bool
	 */
	public static function set_image_seo_webapp_priority( $priority ) {
		return self::set( 'image_seo_webapp_priority', $priority ? '1' : '0' );
	}

	// WooCommerce options.

	/**
	 * Check if product base should be removed from product URLs.
	 *
	 * @return bool
	 */
	public static function is_wc_remove_product_base() {
		return (bool) self::get( 'wc_remove_product_base', false );
	}

	/**
	 * Set whether to remove product base.
	 *
	 * @param bool $enabled Whether to remove product base.
	 * @return bool
	 */
	public static function set_wc_remove_product_base( $enabled ) {
		return self::set( 'wc_remove_product_base', $enabled ? '1' : '0' );
	}

	/**
	 * Check if category base should be removed from product category URLs.
	 *
	 * @return bool
	 */
	public static function is_wc_remove_category_base() {
		return (bool) self::get( 'wc_remove_category_base', false );
	}

	/**
	 * Set whether to remove category base.
	 *
	 * @param bool $enabled Whether to remove category base.
	 * @return bool
	 */
	public static function set_wc_remove_category_base( $enabled ) {
		return self::set( 'wc_remove_category_base', $enabled ? '1' : '0' );
	}

	/**
	 * Check if parent slugs should be removed from category URLs.
	 *
	 * @return bool
	 */
	public static function is_wc_remove_parent_slugs() {
		return (bool) self::get( 'wc_remove_parent_slugs', false );
	}

	/**
	 * Set whether to remove parent slugs.
	 *
	 * @param bool $enabled Whether to remove parent slugs.
	 * @return bool
	 */
	public static function set_wc_remove_parent_slugs( $enabled ) {
		return self::set( 'wc_remove_parent_slugs', $enabled ? '1' : '0' );
	}

	/**
	 * Check if WC generator tag should be removed.
	 *
	 * @return bool
	 */
	public static function is_wc_remove_generator() {
		return (bool) self::get( 'wc_remove_generator', true );
	}

	/**
	 * Set whether to remove WC generator tag.
	 *
	 * @param bool $enabled Whether to remove generator tag.
	 * @return bool
	 */
	public static function set_wc_remove_generator( $enabled ) {
		return self::set( 'wc_remove_generator', $enabled ? '1' : '0' );
	}

	/**
	 * Check if schema markup should be removed from shop archive pages.
	 *
	 * @return bool
	 */
	public static function is_wc_remove_shop_schema() {
		return (bool) self::get( 'wc_remove_shop_schema', true );
	}

	/**
	 * Set whether to remove schema markup from shop archives.
	 *
	 * @param bool $enabled Whether to remove shop schema.
	 * @return bool
	 */
	public static function set_wc_remove_shop_schema( $enabled ) {
		return self::set( 'wc_remove_shop_schema', $enabled ? '1' : '0' );
	}

	/**
	 * Get the brand taxonomy for products.
	 *
	 * @return string
	 */
	public static function get_wc_brand_taxonomy() {
		return self::get( 'wc_brand_taxonomy', '' );
	}

	/**
	 * Set the brand taxonomy for products.
	 *
	 * @param string $taxonomy Taxonomy slug.
	 * @return bool
	 */
	public static function set_wc_brand_taxonomy( $taxonomy ) {
		return self::set( 'wc_brand_taxonomy', sanitize_text_field( $taxonomy ) );
	}

	/**
	 * Get the GTIN identifier type for products.
	 *
	 * @return string One of: gtin8, gtin12, gtin13, gtin14, mpn, isbn.
	 */
	public static function get_wc_gtin_type() {
		return self::get( 'wc_gtin_type', 'gtin8' );
	}

	/**
	 * Set the GTIN identifier type.
	 *
	 * @param string $type Identifier type.
	 * @return bool
	 */
	public static function set_wc_gtin_type( $type ) {
		$valid = array( 'gtin8', 'gtin12', 'gtin13', 'gtin14', 'mpn', 'isbn' );
		if ( ! in_array( $type, $valid, true ) ) {
			$type = 'gtin8';
		}
		return self::set( 'wc_gtin_type', $type );
	}

	/**
	 * Get the custom label for the GTIN field.
	 *
	 * @return string
	 */
	public static function get_wc_gtin_label() {
		return self::get( 'wc_gtin_label', '' );
	}

	/**
	 * Set the custom label for the GTIN field.
	 *
	 * @param string $label Label text.
	 * @return bool
	 */
	public static function set_wc_gtin_label( $label ) {
		return self::set( 'wc_gtin_label', sanitize_text_field( $label ) );
	}

	/**
	 * Check if GTIN should be displayed on the frontend product page.
	 *
	 * @return bool
	 */
	public static function is_wc_show_gtin_frontend() {
		return (bool) self::get( 'wc_show_gtin_frontend', false );
	}

	/**
	 * Set whether to show GTIN on the frontend.
	 *
	 * @param bool $enabled Whether to show GTIN.
	 * @return bool
	 */
	public static function set_wc_show_gtin_frontend( $enabled ) {
		return self::set( 'wc_show_gtin_frontend', $enabled ? '1' : '0' );
	}

	/**
	 * Check if hidden products should be noindexed.
	 *
	 * @return bool
	 */
	public static function is_wc_noindex_hidden_products() {
		return (bool) self::get( 'wc_noindex_hidden_products', false );
	}

	/**
	 * Set whether to noindex hidden products.
	 *
	 * @param bool $enabled Whether to noindex hidden products.
	 * @return bool
	 */
	public static function set_wc_noindex_hidden_products( $enabled ) {
		return self::set( 'wc_noindex_hidden_products', $enabled ? '1' : '0' );
	}

	/**
	 * Get the custom brand text that overrides taxonomy-based brand.
	 *
	 * @return string
	 */
	public static function get_wc_custom_brand() {
		return self::get( 'wc_custom_brand', '' );
	}

	/**
	 * Set the custom brand text.
	 *
	 * @param string $brand Brand text.
	 * @return bool
	 */
	public static function set_wc_custom_brand( $brand ) {
		return self::set( 'wc_custom_brand', sanitize_text_field( $brand ) );
	}

	/**
	 * Check if additional schema properties (manufacturer, brand URL) should be added.
	 *
	 * @return bool
	 */
	public static function is_wc_additional_schema() {
		return (bool) self::get( 'wc_additional_schema', false );
	}

	/**
	 * Set whether to add additional schema properties.
	 *
	 * @param bool $enabled Whether to add additional properties.
	 * @return bool
	 */
	public static function set_wc_additional_schema( $enabled ) {
		return self::set( 'wc_additional_schema', $enabled ? '1' : '0' );
	}

	/**
	 * Get additional currencies for multi-currency schema offers.
	 *
	 * @return array Array of currency codes.
	 */
	public static function get_wc_multicurrency_currencies() {
		$value = self::get( 'wc_multicurrency_currencies', '' );
		if ( empty( $value ) ) {
			return array();
		}
		$currencies = array_filter( array_map( 'trim', explode( ',', $value ) ) );
		return array_map( 'strtoupper', $currencies );
	}

	/**
	 * Set additional currencies for multi-currency schema.
	 *
	 * @param string $currencies Comma-separated currency codes.
	 * @return bool
	 */
	public static function set_wc_multicurrency_currencies( $currencies ) {
		$clean = sanitize_text_field( $currencies );
		return self::set( 'wc_multicurrency_currencies', $clean );
	}

	/**
	 * Check if per-product identifier fields are enabled.
	 *
	 * @return bool
	 */
	public static function is_wc_per_product_identifiers() {
		return (bool) self::get( 'wc_per_product_identifiers', false );
	}

	/**
	 * Set whether per-product identifier fields are enabled.
	 *
	 * @param bool $enabled Whether to enable per-product identifiers.
	 * @return bool
	 */
	public static function set_wc_per_product_identifiers( $enabled ) {
		return self::set( 'wc_per_product_identifiers', $enabled ? '1' : '0' );
	}

	/**
	 * Check if WooCommerce CSV import/export for identifiers is enabled.
	 *
	 * @return bool
	 */
	public static function is_wc_import_export_enabled() {
		return (bool) self::get( 'wc_import_export', false );
	}

	/**
	 * Set whether WooCommerce CSV import/export for identifiers is enabled.
	 *
	 * @param bool $enabled Whether to enable import/export.
	 * @return bool
	 */
	public static function set_wc_import_export_enabled( $enabled ) {
		return self::set( 'wc_import_export', $enabled ? '1' : '0' );
	}

	/**
	 * Check if enhanced WooCommerce schema filtering is enabled.
	 *
	 * @return bool
	 */
	public static function is_wc_enhanced_schema() {
		return (bool) self::get( 'wc_enhanced_schema', false );
	}

	/**
	 * Set whether enhanced WooCommerce schema filtering is enabled.
	 *
	 * @param bool $enabled Whether to enable enhanced schema filtering.
	 * @return bool
	 */
	public static function set_wc_enhanced_schema( $enabled ) {
		return self::set( 'wc_enhanced_schema', $enabled ? '1' : '0' );
	}

	/**
	 * Check if WC breadcrumb schema should be removed in favor of ours.
	 *
	 * @return bool
	 */
	public static function is_wc_remove_breadcrumb_schema() {
		return (bool) self::get( 'wc_remove_breadcrumb_schema', true );
	}

	/**
	 * Set whether WC breadcrumb schema should be removed.
	 *
	 * @param bool $enabled Whether to remove WC breadcrumb schema.
	 * @return bool
	 */
	public static function set_wc_remove_breadcrumb_schema( $enabled ) {
		return self::set( 'wc_remove_breadcrumb_schema', $enabled ? '1' : '0' );
	}

	/**
	 * Check if variation images should be included in product schema.
	 *
	 * @return bool
	 */
	public static function is_wc_schema_variation_images() {
		return (bool) self::get( 'wc_schema_variation_images', false );
	}

	/**
	 * Set whether variation images should be included in product schema.
	 *
	 * @param bool $enabled Whether to include variation images.
	 * @return bool
	 */
	public static function set_wc_schema_variation_images( $enabled ) {
		return self::set( 'wc_schema_variation_images', $enabled ? '1' : '0' );
	}

	/**
	 * Check if product attributes should be included in schema.
	 *
	 * @return bool
	 */
	public static function is_wc_schema_product_attributes() {
		return (bool) self::get( 'wc_schema_product_attributes', false );
	}

	/**
	 * Set whether product attributes should be included in schema.
	 *
	 * @param bool $enabled Whether to include product attributes.
	 * @return bool
	 */
	public static function set_wc_schema_product_attributes( $enabled ) {
		return self::set( 'wc_schema_product_attributes', $enabled ? '1' : '0' );
	}

	/**
	 * Get the return policy page ID for organization schema.
	 *
	 * @return int
	 */
	public static function get_wc_schema_return_policy_page() {
		return absint( self::get( 'wc_schema_return_policy_page', 0 ) );
	}

	/**
	 * Set the return policy page ID for organization schema.
	 *
	 * @param int $page_id Page ID for the return policy page.
	 * @return bool
	 */
	public static function set_wc_schema_return_policy_page( $page_id ) {
		return self::set( 'wc_schema_return_policy_page', absint( $page_id ) );
	}

	/**
	 * Get the list of allowed product attributes for schema output.
	 *
	 * @return array
	 */
	public static function get_wc_allowed_product_attributes() {
		$defaults = array( 'color', 'pattern', 'material', 'size' );
		return apply_filters( 'seovaultseo_allowed_product_attributes', $defaults );
	}

	// Analytics / Tracking Code options.

	/**
	 * Check if GA tracking code injection is enabled.
	 *
	 * @return bool
	 */
	public static function is_ga_tracking_enabled() {
		return (bool) self::get( 'ga_tracking_enabled', false );
	}

	/**
	 * Set GA tracking enabled status.
	 *
	 * @param bool $enabled Whether tracking code injection is enabled.
	 * @return bool
	 */
	public static function set_ga_tracking_enabled( $enabled ) {
		return self::set( 'ga_tracking_enabled', $enabled ? '1' : '0' );
	}

	/**
	 * Get the legacy GA tracking method.
	 *
	 * Free now emits only its generated gtag output. The accessor remains for
	 * one-time upgrade migration and compatibility with existing stored data.
	 *
	 * @return string
	 */
	public static function get_ga_tracking_method() {
		return self::get( 'ga_tracking_method', 'gtag' );
	}

	/**
	 * Set the legacy GA tracking method.
	 *
	 * @param string $method Legacy value; new Free settings always use 'gtag'.
	 * @return bool
	 */
	public static function set_ga_tracking_method( $method ) {
		return self::set( 'ga_tracking_method', $method );
	}

	/**
	 * Get the GA4 Measurement ID (e.g. G-XXXXXXXXXX).
	 *
	 * @return string
	 */
	public static function get_ga_measurement_id() {
		return self::get( 'ga_measurement_id', '' );
	}

	/**
	 * Set the GA4 Measurement ID.
	 *
	 * @param string $id Measurement ID.
	 * @return bool
	 */
	public static function set_ga_measurement_id( $id ) {
		return self::set( 'ga_measurement_id', $id );
	}

	/**
	 * Check if logged-in users should be tracked.
	 *
	 * @return bool
	 */
	public static function is_ga_track_logged_in() {
		return (bool) self::get( 'ga_track_logged_in', true );
	}

	/**
	 * Set whether to track logged-in users.
	 *
	 * @param bool $track Whether to track logged-in users.
	 * @return bool
	 */
	public static function set_ga_track_logged_in( $track ) {
		return self::set( 'ga_track_logged_in', $track ? '1' : '0' );
	}

	/**
	 * Check if IP anonymization is enabled.
	 *
	 * @return bool
	 */
	public static function is_ga_anonymize_ip() {
		return (bool) self::get( 'ga_anonymize_ip', false );
	}

	/**
	 * Set IP anonymization.
	 *
	 * @param bool $anonymize Whether to anonymize IP.
	 * @return bool
	 */
	public static function set_ga_anonymize_ip( $anonymize ) {
		return self::set( 'ga_anonymize_ip', $anonymize ? '1' : '0' );
	}

	// Sitemap options.

	/**
	 * Check if the custom sitemap is enabled.
	 *
	 * @return bool
	 */
	public static function is_sitemap_enabled() {
		return (bool) self::get( 'sitemap_enabled', true );
	}

	/**
	 * Set the custom sitemap enabled status.
	 *
	 * @param bool $enabled Enabled.
	 * @return bool
	 */
	public static function set_sitemap_enabled( $enabled ) {
		return self::set( 'sitemap_enabled', $enabled ? '1' : '0' );
	}

	/**
	 * Get enabled post types for the sitemap.
	 *
	 * @return array
	 */
	public static function get_sitemap_post_types() {
		$types = self::get( 'sitemap_post_types', array() );
		if ( ! is_array( $types ) || empty( $types ) ) {
			return \SEOvaultSEO\Sitemap\Query::get_public_post_types();
		}
		return $types;
	}

	/**
	 * Set enabled post types for the sitemap.
	 *
	 * @param array $types Post type slugs.
	 * @return bool
	 */
	public static function set_sitemap_post_types( $types ) {
		return self::set( 'sitemap_post_types', is_array( $types ) ? $types : array() );
	}

	/**
	 * Get enabled taxonomies for the sitemap.
	 *
	 * @return array
	 */
	public static function get_sitemap_taxonomies() {
		$taxonomies = self::get( 'sitemap_taxonomies', array() );
		if ( ! is_array( $taxonomies ) || empty( $taxonomies ) ) {
			return \SEOvaultSEO\Sitemap\Query::get_public_taxonomies();
		}
		return $taxonomies;
	}

	/**
	 * Set enabled taxonomies for the sitemap.
	 *
	 * @param array $taxonomies Taxonomy slugs.
	 * @return bool
	 */
	public static function set_sitemap_taxonomies( $taxonomies ) {
		return self::set( 'sitemap_taxonomies', is_array( $taxonomies ) ? $taxonomies : array() );
	}

	/**
	 * Check if the user sitemap is enabled.
	 *
	 * @return bool
	 */
	public static function is_sitemap_users_enabled() {
		return (bool) self::get( 'sitemap_users', false );
	}

	/**
	 * Set the user sitemap enabled status.
	 *
	 * @param bool $enabled Enabled.
	 * @return bool
	 */
	public static function set_sitemap_users_enabled( $enabled ) {
		return self::set( 'sitemap_users', $enabled ? '1' : '0' );
	}

	/**
	 * Check if image sitemap is enabled.
	 *
	 * @return bool
	 */
	public static function is_sitemap_images_enabled() {
		return (bool) self::get( 'sitemap_images', true );
	}

	/**
	 * Set image sitemap enabled status.
	 *
	 * @param bool $enabled Enabled.
	 * @return bool
	 */
	public static function set_sitemap_images_enabled( $enabled ) {
		return self::set( 'sitemap_images', $enabled ? '1' : '0' );
	}

	/**
	 * Get sitemap entries per page.
	 *
	 * @return int
	 */
	public static function get_sitemap_entries_per_page() {
		$value = (int) self::get( 'sitemap_entries_per_page', 1000 );
		if ( $value < 100 ) {
			$value = 100;
		}
		if ( $value > 50000 ) {
			$value = 50000;
		}
		return $value;
	}

	/**
	 * Set sitemap entries per page.
	 *
	 * @param int $per_page Entries per page.
	 * @return bool
	 */
	public static function set_sitemap_entries_per_page( $per_page ) {
		$per_page = (int) $per_page;
		if ( $per_page < 100 ) {
			$per_page = 100;
		}
		if ( $per_page > 50000 ) {
			$per_page = 50000;
		}
		return self::set( 'sitemap_entries_per_page', $per_page );
	}

	/**
	 * Check if news sitemap is enabled.
	 *
	 * @return bool
	 */
	public static function is_sitemap_news_enabled() {
		return (bool) self::get( 'sitemap_news', false );
	}

	/**
	 * Set news sitemap enabled status.
	 *
	 * @param bool $enabled Enabled.
	 * @return bool
	 */
	public static function set_sitemap_news_enabled( $enabled ) {
		return self::set( 'sitemap_news', $enabled ? '1' : '0' );
	}

	/**
	 * Get sitemap cron frequency.
	 *
	 * @return string
	 */
	public static function get_sitemap_cron_frequency() {
		return self::get( 'sitemap_cron_frequency', 'every_six_hours' );
	}

	/**
	 * Set sitemap cron frequency.
	 *
	 * @param string $frequency Frequency.
	 * @return bool
	 */
	public static function set_sitemap_cron_frequency( $frequency ) {
		$valid = array( 'every_six_hours', 'hourly', 'twicedaily', 'daily' );
		if ( ! in_array( $frequency, $valid, true ) ) {
			$frequency = 'every_six_hours';
		}
		return self::set( 'sitemap_cron_frequency', $frequency );
	}

	/**
	 * Get the last generated sitemap timestamp.
	 *
	 * @return int
	 */
	public static function get_sitemap_last_generated() {
		return (int) self::get( 'sitemap_last_generated', 0 );
	}

	/**
	 * Set the last generated sitemap timestamp.
	 *
	 * @param int $timestamp Timestamp.
	 * @return bool
	 */
	public static function set_sitemap_last_generated( $timestamp ) {
		return self::set( 'sitemap_last_generated', (int) $timestamp );
	}

	// Autopilot options.

	/**
	 * Check if autopilot is enabled.
	 *
	 * @return bool
	 */
	public static function is_autopilot_enabled() {
		return 'yes' === self::get( 'autopilot_enabled', 'yes' );
	}

	/**
	 * Set autopilot enabled status.
	 *
	 * @param string $status 'yes' or 'no'.
	 * @return bool
	 */
	public static function set_autopilot_enabled( $status ) {
		return self::set( 'autopilot_enabled', $status );
	}

	/**
	 * Check if search pages should be noindexed.
	 *
	 * @return bool
	 */
	public static function is_search_noindex() {
		return 'yes' === self::get( 'search_noindex', 'yes' );
	}

	/**
	 * Set search noindex status.
	 *
	 * @param string $status 'yes' or 'no'.
	 * @return bool
	 */
	public static function set_search_noindex( $status ) {
		return self::set( 'search_noindex', $status );
	}

	/**
	 * Post types that should be noindexed by default (singular views).
	 *
	 * Per-post `_seovault_robots_noindex` meta overrides this list when present.
	 *
	 * @return array
	 */
	public static function get_noindex_post_types() {
		$types = self::get( 'noindex_post_types', array() );
		return is_array( $types ) ? array_values( $types ) : array();
	}

	/**
	 * Set post types that should be noindexed by default.
	 *
	 * @param array $types Post type slugs.
	 * @return bool
	 */
	public static function set_noindex_post_types( $types ) {
		return self::set( 'noindex_post_types', is_array( $types ) ? array_values( $types ) : array() );
	}

	/**
	 * Whether a post type is configured for default noindex.
	 *
	 * @param string $post_type Post type slug.
	 * @return bool
	 */
	public static function is_post_type_noindex( $post_type ) {
		$post_type = (string) $post_type;
		if ( '' === $post_type ) {
			return false;
		}
		return in_array( $post_type, self::get_noindex_post_types(), true );
	}

	/**
	 * Check if date archives should be noindexed.
	 *
	 * @return bool
	 */
	public static function is_date_archives_noindex() {
		return 'yes' === self::get( 'date_archives_noindex', 'yes' );
	}

	/**
	 * Set date archives noindex status.
	 *
	 * @param string $status 'yes' or 'no'.
	 * @return bool
	 */
	public static function set_date_archives_noindex( $status ) {
		return self::set( 'date_archives_noindex', $status );
	}

	/**
	 * Check if attachment pages should be noindexed.
	 *
	 * @return bool
	 */
	public static function is_attachment_noindex() {
		return 'yes' === self::get( 'attachment_noindex', 'yes' );
	}

	/**
	 * Set attachment noindex status.
	 *
	 * @param string $status 'yes' or 'no'.
	 * @return bool
	 */
	public static function set_attachment_noindex( $status ) {
		return self::set( 'attachment_noindex', $status );
	}

	/**
	 * Check if post format archives should be noindexed.
	 *
	 * @return bool
	 */
	public static function is_post_format_noindex() {
		return 'yes' === self::get( 'post_format_noindex', 'yes' );
	}

	/**
	 * Set post format noindex status.
	 *
	 * @param string $status 'yes' or 'no'.
	 * @return bool
	 */
	public static function set_post_format_noindex( $status ) {
		return self::set( 'post_format_noindex', $status );
	}

	/**
	 * Check if empty archives should be noindexed.
	 *
	 * @return bool
	 */
	public static function is_empty_archives_noindex() {
		return 'yes' === self::get( 'empty_archives_noindex', 'yes' );
	}

	/**
	 * Set empty archives noindex status.
	 *
	 * @param string $status 'yes' or 'no'.
	 * @return bool
	 */
	public static function set_empty_archives_noindex( $status ) {
		return self::set( 'empty_archives_noindex', $status );
	}

	/**
	 * Check if advanced query protection is enabled.
	 *
	 * @return bool
	 */
	public static function is_advanced_query_protection() {
		return 'yes' === self::get( 'advanced_query_protection', 'yes' );
	}

	/**
	 * Set advanced query protection status.
	 *
	 * @param string $status 'yes' or 'no'.
	 * @return bool
	 */
	public static function set_advanced_query_protection( $status ) {
		return self::set( 'advanced_query_protection', $status );
	}

	/**
	 * Get robots max-snippet length.
	 *
	 * @return int -1 = unlimited, 0 = disabled, positive = character limit.
	 */
	public static function get_robots_max_snippet() {
		return (int) self::get( 'robots_max_snippet', -1 );
	}

	/**
	 * Set robots max-snippet length.
	 *
	 * @param int $value -1 = unlimited, 0 = disabled, positive = character limit.
	 * @return bool
	 */
	public static function set_robots_max_snippet( $value ) {
		return self::set( 'robots_max_snippet', (int) $value );
	}

	/**
	 * Get robots max-image-preview size.
	 *
	 * @return string 'none', 'standard', or 'large'.
	 */
	public static function get_robots_max_image_preview() {
		return self::get( 'robots_max_image_preview', 'large' );
	}

	/**
	 * Set robots max-image-preview size.
	 *
	 * @param string $value 'none', 'standard', or 'large'.
	 * @return bool
	 */
	public static function set_robots_max_image_preview( $value ) {
		return self::set( 'robots_max_image_preview', $value );
	}

	/**
	 * Get robots max-video-preview length.
	 *
	 * @return int -1 = unlimited, 0 = disabled, positive = seconds limit.
	 */
	public static function get_robots_max_video_preview() {
		return (int) self::get( 'robots_max_video_preview', -1 );
	}

	/**
	 * Set robots max-video-preview length.
	 *
	 * @param int $value -1 = unlimited, 0 = disabled, positive = seconds limit.
	 * @return bool
	 */
	public static function set_robots_max_video_preview( $value ) {
		return self::set( 'robots_max_video_preview', (int) $value );
	}

	/**
	 * Check if Open Graph output is enabled.
	 *
	 * @return bool
	 */
	public static function is_og_enabled() {
		return 'yes' === self::get( 'og_enabled', 'yes' );
	}

	/**
	 * Set Open Graph enabled status.
	 *
	 * @param string $status 'yes' or 'no'.
	 * @return bool
	 */
	public static function set_og_enabled( $status ) {
		return self::set( 'og_enabled', $status );
	}

	/**
	 * Check if Twitter card output is enabled.
	 *
	 * @return bool
	 */
	public static function is_twitter_enabled() {
		return 'yes' === self::get( 'twitter_enabled', 'yes' );
	}

	/**
	 * Set Twitter card enabled status.
	 *
	 * @param string $status 'yes' or 'no'.
	 * @return bool
	 */
	public static function set_twitter_enabled( $status ) {
		return self::set( 'twitter_enabled', $status );
	}

	/**
	 * Check if schema output is enabled.
	 *
	 * @return bool
	 */
	public static function is_schema_enabled() {
		return 'yes' === self::get( 'schema_enabled', 'yes' );
	}

	/**
	 * Set schema enabled status.
	 *
	 * @param string $status 'yes' or 'no'.
	 * @return bool
	 */
	public static function set_schema_enabled( $status ) {
		return self::set( 'schema_enabled', $status );
	}

	/**
	 * Check if noindex content should be excluded from search.
	 *
	 * @return bool
	 */
	public static function is_exclude_noindex_from_search() {
		return 'yes' === self::get( 'exclude_noindex_from_search', 'yes' );
	}

	/**
	 * Set exclude noindex from search status.
	 *
	 * @param string $status 'yes' or 'no'.
	 * @return bool
	 */
	public static function set_exclude_noindex_from_search( $status ) {
		return self::set( 'exclude_noindex_from_search', $status );
	}

	// Feed options.

	/**
	 * Check if feed excerpting is enabled.
	 *
	 * @return bool
	 */
	public static function is_feed_excerpt_enabled() {
		return (bool) self::get( 'feed_excerpt_enabled', false );
	}

	/**
	 * Set feed excerpt enabled status.
	 *
	 * @param bool $enabled Whether to excerpt feed content.
	 * @return bool
	 */
	public static function set_feed_excerpt_enabled( $enabled ) {
		return self::set( 'feed_excerpt_enabled', $enabled ? '1' : '0' );
	}

	/**
	 * Get feed excerpt length (max character count).
	 *
	 * @return int
	 */
	public static function get_feed_excerpt_length() {
		$length = (int) self::get( 'feed_excerpt_length', 400 );
		if ( $length < 1 ) {
			return 400;
		}
		return $length;
	}

	/**
	 * Set feed excerpt length.
	 *
	 * @param int $length Max character count.
	 * @return bool
	 */
	public static function set_feed_excerpt_length( $length ) {
		$length = (int) $length;
		if ( $length < 1 ) {
			$length = 400;
		}
		return self::set( 'feed_excerpt_length', $length );
	}

	/**
	 * Check if source backlink should be appended to feed content.
	 *
	 * @return bool
	 */
	public static function is_feed_source_link_enabled() {
		return (bool) self::get( 'feed_source_link', false );
	}

	/**
	 * Set feed source backlink enabled status.
	 *
	 * @param bool $enabled Whether to append source link.
	 * @return bool
	 */
	public static function set_feed_source_link_enabled( $enabled ) {
		return self::set( 'feed_source_link', $enabled ? '1' : '0' );
	}

	/**
	 * Check if X-Robots-Tag: noindex should be sent for feeds.
	 *
	 * @return bool
	 */
	public static function is_feed_noindex() {
		return (bool) self::get( 'feed_noindex', false );
	}

	/**
	 * Set feed noindex header status.
	 *
	 * @param bool $enabled Whether to send noindex header.
	 * @return bool
	 */
	public static function set_feed_noindex( $enabled ) {
		return self::set( 'feed_noindex', $enabled ? '1' : '0' );
	}

	// oEmbed options.

	/**
	 * Check if SEO/OG title should be used for oEmbed title.
	 *
	 * @return bool
	 */
	public static function is_oembed_use_seo_title() {
		return (bool) self::get( 'oembed_use_seo_title', false );
	}

	/**
	 * Set whether to use SEO/OG title for oEmbed title.
	 *
	 * @param bool $enabled Whether to override oEmbed title.
	 * @return bool
	 */
	public static function set_oembed_use_seo_title( $enabled ) {
		return self::set( 'oembed_use_seo_title', $enabled ? '1' : '0' );
	}

	/**
	 * Check if social image should be used for oEmbed thumbnails.
	 *
	 * @return bool
	 */
	public static function is_oembed_use_social_image() {
		return (bool) self::get( 'oembed_use_social_image', false );
	}

	/**
	 * Set whether to use social image for oEmbed thumbnails.
	 *
	 * @param bool $enabled Whether to override oEmbed thumbnail.
	 * @return bool
	 */
	public static function set_oembed_use_social_image( $enabled ) {
		return self::set( 'oembed_use_social_image', $enabled ? '1' : '0' );
	}

	/**
	 * Check if author info should be removed from oEmbed payload.
	 *
	 * @return bool
	 */
	public static function is_oembed_remove_author() {
		return (bool) self::get( 'oembed_remove_author', false );
	}

	/**
	 * Set whether to remove author info from oEmbed payload.
	 *
	 * @param bool $enabled Whether to remove author info.
	 * @return bool
	 */
	public static function set_oembed_remove_author( $enabled ) {
		return self::set( 'oembed_remove_author', $enabled ? '1' : '0' );
	}

	/**
	 * Whether full data erasure should run when the plugin is uninstalled.
	 *
	 * @return bool
	 */
	public static function is_erase_on_uninstall() {
		return 'yes' === self::get( 'erase_on_uninstall', 'no' );
	}

	/**
	 * Set whether to erase all plugin data on uninstall.
	 *
	 * @param bool $enabled Whether full wipe is enabled.
	 * @return bool
	 */
	public static function set_erase_on_uninstall( $enabled ) {
		return self::set( 'erase_on_uninstall', $enabled ? 'yes' : 'no', false );
	}
}
