<?php
/**
 * WooCommerce Integration
 *
 * Handles SEO output for WooCommerce products: product schema,
 * Open Graph product tags, meta description fallbacks, robots
 * directives for cart/checkout, sitemap exclusions, permalink
 * optimization, and template variable registration.
 *
 * @package SEOvaultSEO\WooCommerce
 */

namespace SEOvaultSEO\WooCommerce;

use SEOvaultSEO\Core\Options;
use SEOvaultSEO\ReplaceVariables\Manager;

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

class WooCommerce {

	/**
	 * Singleton instance.
	 *
	 * @var WooCommerce|null
	 */
	private static $instance = null;

	/**
	 * Cached product object for current request.
	 *
	 * @var \WC_Product|null
	 */
	private $product = null;

	/**
	 * True while build_product_schema is in progress (blocks re-entrant builds).
	 *
	 * @var bool
	 */
	private static $building_product_schema = false;

	/**
	 * Get singleton.
	 *
	 * @return WooCommerce
	 */
	public static function get_instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor — registers hooks.
	 */
	private function __construct() {
		// Register template variables.
		add_action( 'seovaultseo/vars/register_extra', array( $this, 'register_variables' ) );

		// Admin hooks.
		if ( is_admin() ) {
			// GTIN field on product edit page.
			add_action( 'woocommerce_product_options_sku', array( $this, 'render_gtin_field' ) );
			add_action( 'woocommerce_product_after_variable_attributes', array( $this, 'render_variation_gtin_field' ), 10, 3 );
			add_action( 'woocommerce_admin_process_product_object', array( $this, 'save_gtin_data' ) );
			add_action( 'woocommerce_save_product_variation', array( $this, 'save_variation_gtin_data' ), 10, 2 );

			// Per-product identifier fields (all types simultaneously).
			if ( Options::is_wc_per_product_identifiers() ) {
				Product_Identifiers::get_instance();
			}

			// CSV Import/Export for product identifiers.
			if ( Options::is_wc_import_export_enabled() ) {
				Import_Export::get_instance();
			}
		}

		// Canonical Product schema is a tagged V2 integration contribution
		// (not an untagged seovaultseo_set_schema extension). Register even in
		// wp-admin so save-time validation can collect Product for product posts.
		if ( \SEOvaultSEO\Compatibility\Detector::should_output_seo() ) {
			ProductSchemaContributor::register();
			Schema_Filter::get_instance();
		}

		// Frontend-only hooks.
		if ( ! is_admin() && \SEOvaultSEO\Compatibility\Detector::should_output_seo() ) {
			// Open Graph product type.
			add_filter( 'seovaultseo_og_type', array( $this, 'product_og_type' ) );

			// Open Graph product tags.
			add_action( 'wp_head', array( $this, 'output_product_og_tags' ), 12 );

			// Meta description fallback for products.
			add_filter( 'seovaultseo_description', array( $this, 'product_meta_description' ), 10, 2 );

			// Robots for cart/checkout/account pages.
			add_filter( 'seovaultseo_robots_meta', array( $this, 'cart_checkout_robots' ) );

			// Robots for hidden products.
			add_filter( 'seovaultseo_robots_meta', array( $this, 'hidden_products_robots' ), 5 );

			// Sitemap exclusions.
			add_filter( 'wp_sitemaps_posts_query', array( $this, 'exclude_wc_post_types' ), 15, 2 );
			add_filter( 'wp_sitemaps_taxonomies_query_args', array( $this, 'exclude_wc_taxonomies' ), 15, 2 );

			// Remove WC generator tag if configured.
			if ( Options::is_wc_remove_generator() ) {
				remove_action( 'get_the_generator_html', 'wc_generator_tag', 10 );
				remove_action( 'get_the_generator_xhtml', 'wc_generator_tag', 10 );
			}

			// Permalink optimization.
			$this->init_permalink_optimization();

			// Show GTIN on the frontend product page if enabled.
			if ( Options::is_wc_show_gtin_frontend() ) {
				add_action( 'woocommerce_product_meta_start', array( $this, 'render_gtin_frontend' ) );
			}
		}
	}

	/**
	 * Initialize the plugin.
	 *
	 * @return void
	 */
	public static function init() {
		if ( ! self::is_woocommerce_active() ) {
			return;
		}
		self::get_instance();
	}

	/**
	 * Check if WooCommerce is active.
	 *
	 * @return bool
	 */
	public static function is_woocommerce_active() {
		return class_exists( 'WooCommerce' ) || function_exists( 'wc_get_product' );
	}

	// =========================================================================
	// Template Variables
	// =========================================================================

	/**
	 * Register WooCommerce template variables.
	 *
	 * @return void
	 */
	public function register_variables() {
		$manager = Manager::get_instance();

		$manager->register_replacement(
			'wc_price',
			array(
				'name'        => __( 'Product Price', 'seovault-ai' ),
				'description' => __( 'Formatted price of the current product.', 'seovault-ai' ),
				'variable'    => 'wc_price',
				'example'     => $this->get_product_price(),
			),
			array( $this, 'get_product_price' )
		);

		$manager->register_replacement(
			'wc_sku',
			array(
				'name'        => __( 'Product SKU', 'seovault-ai' ),
				'description' => __( 'SKU of the current product.', 'seovault-ai' ),
				'variable'    => 'wc_sku',
				'example'     => $this->get_product_sku(),
			),
			array( $this, 'get_product_sku' )
		);

		$manager->register_replacement(
			'wc_shortdesc',
			array(
				'name'        => __( 'Product Short Description', 'seovault-ai' ),
				'description' => __( 'Short description of the current product.', 'seovault-ai' ),
				'variable'    => 'wc_shortdesc',
				'example'     => $this->get_short_description(),
			),
			array( $this, 'get_short_description' )
		);

		$manager->register_replacement(
			'wc_brand',
			array(
				'name'        => __( 'Product Brand', 'seovault-ai' ),
				'description' => __( 'Brand taxonomy term of the current product.', 'seovault-ai' ),
				'variable'    => 'wc_brand',
				'example'     => $this->get_product_brand(),
			),
			array( $this, 'get_product_brand' )
		);
	}

	// =========================================================================
	// Product Helpers
	// =========================================================================

	/**
	 * Get the WC_Product for the current context.
	 *
	 * @param int $product_id Optional product ID. Defaults to queried object.
	 * @return \WC_Product|null
	 */
	public function get_product( $product_id = 0 ) {
		if ( ! self::is_woocommerce_active() ) {
			return null;
		}

		if ( $product_id ) {
			return $this->get_product_by_id( $product_id );
		}

		if ( null !== $this->product ) {
			return $this->product;
		}

		$product_id = get_queried_object_id();

		if ( ! $product_id || ! is_singular( 'product' ) ) {
			return null;
		}

		$this->product = $this->get_product_by_id( $product_id );
		return $this->product;
	}

	/**
	 * Get a product by ID.
	 *
	 * @param int $product_id Product ID.
	 * @return \WC_Product|null
	 */
	public function get_product_by_id( $product_id ) {
		$product_id = (int) $product_id;

		if ( $product_id <= 0 ) {
			return null;
		}

		if ( function_exists( 'wc_get_product' ) ) {
			$product = wc_get_product( $product_id );

			return ( $product && is_object( $product ) ) ? $product : null;
		}

		return null;
	}

	/**
	 * Get product short description.
	 *
	 * @param \WC_Product|null $product Product object or null for current.
	 * @return string
	 */
	public function get_short_description( $product = null ) {
		if ( null === $product ) {
			$product = $this->get_product();
		}

		if ( ! is_object( $product ) ) {
			return '';
		}

		if ( method_exists( $product, 'get_short_description' ) ) {
			return wp_strip_all_tags( $product->get_short_description(), true );
		}

		return '';
	}

	/**
	 * Get product long description.
	 *
	 * @param \WC_Product|null $product Product object or null for current.
	 * @return string
	 */
	public function get_long_description( $product = null ) {
		if ( null === $product ) {
			$product = $this->get_product();
		}

		if ( ! is_object( $product ) ) {
			return '';
		}

		if ( method_exists( $product, 'get_description' ) ) {
			return wp_strip_all_tags( $product->get_description(), true );
		}

		return '';
	}

	/**
	 * Get formatted product price.
	 *
	 * @return string
	 */
	public function get_product_price() {
		$product = $this->get_product();

		if ( ! is_object( $product ) ) {
			return '';
		}

		if ( method_exists( $product, 'get_price' ) ) {
			$price = $product->get_price();

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

			if ( function_exists( 'wc_price' ) ) {
				return wp_strip_all_tags( html_entity_decode( wc_price( $price ) ), true );
			}

			return (string) $price;
		}

		return '';
	}

	/**
	 * Get product SKU.
	 *
	 * @return string
	 */
	public function get_product_sku() {
		$product = $this->get_product();

		if ( ! is_object( $product ) ) {
			return '';
		}

		if ( method_exists( $product, 'get_sku' ) ) {
			return $product->get_sku();
		}

		return '';
	}

	/**
	 * Get product brand from configured taxonomy.
	 *
	 * @param int $product_id Optional product ID.
	 * @return string
	 */
	public function get_product_brand( $product_id = 0 ) {
		if ( ! $product_id ) {
			$product = $this->get_product();
			if ( ! is_object( $product ) ) {
				return '';
			}
			$product_id = $product->get_id();
		}

		return self::get_brand( $product_id );
	}

	/**
	 * Get brand for a product from the configured taxonomy.
	 *
	 * @param int $product_id Product ID.
	 * @return string
	 */
	public static function get_brand( $product_id ) {
		$product_id = absint( $product_id );

		if ( ! $product_id ) {
			return '';
		}

		$taxonomy = Options::get_wc_brand_taxonomy();

		if ( ! $taxonomy || ! taxonomy_exists( $taxonomy ) ) {
			return '';
		}

		$brands = get_the_terms( $product_id, $taxonomy );

		if ( is_wp_error( $brands ) || empty( $brands ) || ! is_array( $brands ) ) {
			return '';
		}

		$brand = $brands[0]->name ?? '';

		return apply_filters( 'seovaultseo_wc_product_brand', $brand, $product_id );
	}

	// =========================================================================
	// GTIN / MPN / ISBN Fields
	// =========================================================================

	/**
	 * Meta key used to store the GTIN/MPN/ISBN value.
	 */
	const GTIN_META_KEY = '_seovaultseo_gtin_code';

	/**
	 * Get the GTIN/MPN/ISBN value for a product.
	 *
	 * Checks our custom meta field first, then falls back to WC's native
	 * global_unique_id if available.
	 *
	 * @param \WC_Product|null $product Product object.
	 * @return string
	 */
	public function get_gtin_value( $product ) {
		if ( ! is_object( $product ) ) {
			return '';
		}

		// Check our custom meta field first.
		if ( method_exists( $product, 'get_meta' ) ) {
			$custom = $product->get_meta( self::GTIN_META_KEY );
			if ( ! empty( $custom ) ) {
				return $custom;
			}
		}

		// Fall back to WC native global unique ID.
		if ( method_exists( $product, 'get_global_unique_id' ) ) {
			return (string) $product->get_global_unique_id();
		}

		return '';
	}

	/**
	 * Get the GTIN/MPN/ISBN value by product ID from post meta.
	 *
	 * @param int $product_id Product ID.
	 * @return string
	 */
	public function get_gtin_by_product_id( $product_id ) {
		$product_id = absint( $product_id );

		if ( ! $product_id ) {
			return '';
		}

		$gtin = get_post_meta( $product_id, self::GTIN_META_KEY, true );

		if ( ! empty( $gtin ) ) {
			return $gtin;
		}

		// Fall back to WC native field.
		$product = $this->get_product_by_id( $product_id );
		if ( is_object( $product ) && method_exists( $product, 'get_global_unique_id' ) ) {
			return (string) $product->get_global_unique_id();
		}

		return '';
	}

	/**
	 * Get a human-readable label for the configured GTIN type.
	 *
	 * @return string
	 */
	public function get_gtin_label() {
		$type  = Options::get_wc_gtin_type();
		$label = Options::get_wc_gtin_label();

		if ( ! empty( $label ) ) {
			return $label;
		}

		$defaults = array(
			'gtin8'  => __( 'GTIN', 'seovault-ai' ),
			'gtin12' => __( 'GTIN', 'seovault-ai' ),
			'gtin13' => __( 'GTIN', 'seovault-ai' ),
			'gtin14' => __( 'GTIN', 'seovault-ai' ),
			'mpn'    => __( 'MPN', 'seovault-ai' ),
			'isbn'   => __( 'ISBN', 'seovault-ai' ),
		);

		return $defaults[ $type ] ?? __( 'GTIN', 'seovault-ai' );
	}

	/**
	 * Render the GTIN field on the product edit page (Inventory tab).
	 *
	 * @return void
	 */
	public function render_gtin_field() {
		if ( ! function_exists( 'woocommerce_wp_text_input' ) ) {
			return;
		}

		$label = $this->get_gtin_label();

		woocommerce_wp_text_input(
			array(
				'id'          => self::GTIN_META_KEY,
				'label'       => $label,
				'description' => sprintf(
					/* translators: %s = identifier label */
					__( '%s value used in the Product Schema. Overrides the WooCommerce global unique ID field.', 'seovault-ai' ),
					$label
				),
			)
		);
	}

	/**
	 * Render the GTIN field for each variation.
	 *
	 * @param int    $loop           Variation loop index.
	 * @param array  $variation_data Variation data array.
	 * @param object $variation      Variation object.
	 * @return void
	 */
	public function render_variation_gtin_field( $loop, $variation_data, $variation ) {
		if ( ! function_exists( 'woocommerce_wp_text_input' ) ) {
			return;
		}

		$label            = $this->get_gtin_label();
		$variation_object = function_exists( 'wc_get_product' ) ? wc_get_product( $variation->ID ) : null;
		$value            = '';

		if ( is_object( $variation_object ) && method_exists( $variation_object, 'get_meta' ) ) {
			$value = $variation_object->get_meta( self::GTIN_META_KEY );
		}

		woocommerce_wp_text_input(
			array(
				'id'            => self::GTIN_META_KEY . "_variable{$loop}",
				'name'          => self::GTIN_META_KEY . "_variable[{$loop}]",
				'value'         => $value,
				'label'         => $label,
				'description'   => sprintf(
					/* translators: %s = identifier label */
					__( '%s value used in the Product Schema. Overrides the WooCommerce global unique ID field.', 'seovault-ai' ),
					$label
				),
				'wrapper_class' => 'form-row widefat',
			)
		);
	}

	/**
	 * Save GTIN data when a product is saved in admin.
	 *
	 * @param \WC_Product $product Product object being saved.
	 * @return void
	 */
	public function save_gtin_data( $product ) {
		if ( ! is_object( $product ) || ! method_exists( $product, 'get_id' ) || ! method_exists( $product, 'update_meta_data' ) ) {
			return;
		}

		if ( ! isset( $_POST[ self::GTIN_META_KEY ] ) ) {
			return;
		}

		$product_id = absint( $product->get_id() );
		$nonce      = isset( $_POST['woocommerce_meta_nonce'] )
			? sanitize_text_field( wp_unslash( $_POST['woocommerce_meta_nonce'] ) )
			: '';
		if ( ! $product_id || ! wp_verify_nonce( $nonce, 'woocommerce_save_data' ) || ! current_user_can( 'edit_post', $product_id ) ) {
			return;
		}

		$value = sanitize_text_field( wp_unslash( $_POST[ self::GTIN_META_KEY ] ) );
		$product->update_meta_data( self::GTIN_META_KEY, $value );
	}

	/**
	 * Save GTIN data for a specific variation.
	 *
	 * @param int $variation_id Variation ID.
	 * @param int $index        Variation index in the form.
	 * @return void
	 */
	public function save_variation_gtin_data( $variation_id, $index ) {
		$variation_id = absint( $variation_id );

		if ( ! $variation_id ) {
			return;
		}

		if ( ! isset( $_POST[ self::GTIN_META_KEY . '_variable' ] ) ) {
			return;
		}

		$nonce = isset( $_POST['woocommerce_meta_nonce'] )
			? sanitize_text_field( wp_unslash( $_POST['woocommerce_meta_nonce'] ) )
			: '';
		if ( ! wp_verify_nonce( $nonce, 'woocommerce_save_data' ) || ! current_user_can( 'edit_post', $variation_id ) ) {
			return;
		}

		$all_values = is_array( $_POST[ self::GTIN_META_KEY . '_variable' ] )
			? map_deep( wp_unslash( $_POST[ self::GTIN_META_KEY . '_variable' ] ), 'sanitize_text_field' )
			: array();
		$value      = isset( $all_values[ $index ] ) ? sanitize_text_field( $all_values[ $index ] ) : '';

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

		$variation = function_exists( 'wc_get_product' ) ? wc_get_product( $variation_id ) : null;

		if ( is_object( $variation ) && method_exists( $variation, 'update_meta_data' ) ) {
			$variation->update_meta_data( self::GTIN_META_KEY, $value );
			if ( method_exists( $variation, 'save_meta_data' ) ) {
				$variation->save_meta_data();
			}
		} else {
			update_post_meta( $variation_id, self::GTIN_META_KEY, $value );
		}
	}

	/**
	 * Render the GTIN value on the frontend product page.
	 *
	 * @return void
	 */
	public function render_gtin_frontend() {
		$product = $this->get_product();

		if ( ! is_object( $product ) ) {
			return;
		}

		$gtin = $this->get_gtin_value( $product );

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

		$label = $this->get_gtin_label();

		echo '<span class="seovaultseo-gtin-wrapper">';
		echo esc_html( $label . ': ' . $gtin );
		echo '</span>';
	}

	// =========================================================================
	// Custom Brand Override
	// =========================================================================

	/**
	 * Get the brand for a product, checking custom brand text first.
	 *
	 * @param int $product_id Product ID.
	 * @return string
	 */
	public function get_product_brand_with_override( $product_id ) {
		$custom_brand = Options::get_wc_custom_brand();

		if ( ! empty( $custom_brand ) ) {
			return apply_filters( 'seovaultseo_wc_product_brand', $custom_brand, $product_id );
		}

		return self::get_brand( $product_id );
	}

	// =========================================================================
	// Robots for Hidden Products
	// =========================================================================

	/**
	 * Set noindex for hidden products (catalog visibility = hidden).
	 *
	 * @param string $robots Current robots meta content.
	 * @return string
	 */
	public function hidden_products_robots( $robots ) {
		if ( ! Options::is_wc_noindex_hidden_products() ) {
			return $robots;
		}

		if ( ! is_singular( 'product' ) ) {
			return $robots;
		}

		$product = $this->get_product();

		if ( ! is_object( $product ) ) {
			return $robots;
		}

		if ( method_exists( $product, 'get_catalog_visibility' ) && 'hidden' === $product->get_catalog_visibility() ) {
			return 'noindex, follow';
		}

		return $robots;
	}

	// =========================================================================
	// Additional Schema Properties
	// =========================================================================

	/**
	 * Add manufacturer and brand URL to the product schema.
	 *
	 * @param array $schema   Product schema array.
	 * @param int   $post_id  Product post ID.
	 * @return array
	 */
	public function add_additional_schema_properties( $schema, $post_id ) {
		$post_id = (int) $post_id;

		if ( $post_id <= 0 ) {
			return $schema;
		}

		// Manufacturer — reference the organization/person schema node.
		$schema['manufacturer'] = array(
			'@id' => home_url( '/#organization' ),
		);

		// Canonical product URL.
		$schema['url'] = get_permalink( $post_id );

		// Brand URL if brand taxonomy is set and brand exists.
		$taxonomy = Options::get_wc_brand_taxonomy();
		if ( ! empty( $schema['brand'] ) && $taxonomy && taxonomy_exists( $taxonomy ) ) {
			$brands = get_the_terms( $post_id, $taxonomy );
			if ( ! is_wp_error( $brands ) && ! empty( $brands[0] ) ) {
				$schema['brand']['url'] = get_term_link( $brands[0], $taxonomy );
			}
		}

		return $schema;
	}

	// =========================================================================
	// Multi-Currency Offers
	// =========================================================================

	/**
	 * Add offers for additional currencies to the product schema.
	 *
	 * Converts the single-currency offer into an array of offers, one per
	 * currency. The base currency offer is always kept.
	 *
	 * @param array $offers     Current offers array or single offer.
	 * @param array $currencies Additional currency codes.
	 * @return array
	 */
	public function add_multicurrency_offers( $offers, $currencies ) {
		if ( empty( $currencies ) || ! is_array( $currencies ) ) {
			return $offers;
		}

		// Normalize to an array of offers.
		$single = false;
		if ( isset( $offers['@type'] ) ) {
			$offers = array( $offers );
			$single = true;
		}

		if ( empty( $offers ) || ! is_array( $offers ) ) {
			return $offers;
		}

		$base_offer    = $offers[0];
		$base_currency = $base_offer['priceCurrency'] ?? '';
		$all_offers    = array( $base_offer );

		foreach ( $currencies as $currency ) {
			$currency = strtoupper( trim( $currency ) );

			// Skip if same as base currency.
			if ( $currency === strtoupper( $base_currency ) ) {
				continue;
			}

			$extra_offer                  = $base_offer;
			$extra_offer['priceCurrency'] = $currency;

			/**
			 * Filter the price for a specific currency in multi-currency schema.
			 *
			 * @param float|string $price    Price value (default: base price).
			 * @param string       $currency Currency code.
			 * @param array        $base_offer Base offer data.
			 */
			$converted_price = apply_filters( 'seovaultseo_wc_multicurrency_price', $base_offer['price'], $currency, $base_offer );

			if ( ! empty( $converted_price ) ) {
				$extra_offer['price'] = $converted_price;
			}

			$all_offers[] = $extra_offer;
		}

		return $all_offers;
	}

	// =========================================================================
	// Variable Product Schema (ProductGroup)
	// =========================================================================

	/**
	 * Add ProductGroup variant data to the schema for variable products.
	 *
	 * @param array       $schema  Product schema array.
	 * @param \WC_Product $product Product object.
	 * @param int         $post_id Product post ID.
	 * @return array
	 */
	public function add_variable_product_schema( $schema, $product, $post_id ) {
		if ( ! is_object( $product ) || ! method_exists( $product, 'get_available_variations' ) ) {
			return $schema;
		}

		$variations = $product->get_available_variations( 'object' );

		if ( empty( $variations ) || ! is_array( $variations ) ) {
			return $schema;
		}

		// Convert to ProductGroup.
		$schema['@type']          = 'ProductGroup';
		$schema['productGroupID'] = ! empty( $schema['sku'] ) ? $schema['sku'] : (string) $post_id;

		$varies_by = array();
		$variants  = array();

		foreach ( $variations as $variation ) {
			$variant = $this->build_variant_data( $variation, $product );

			if ( ! empty( $variant ) ) {
				$variants[] = $variant;

				// Collect variesBy attributes.
				if ( method_exists( $variation, 'get_attributes' ) ) {
					$attributes = $variation->get_attributes();
					if ( is_array( $attributes ) ) {
						foreach ( $attributes as $key => $value ) {
							if ( '' === $value && '0' !== $value ) {
								continue;
							}
							$key = str_replace( 'pa_', '', $key );
							if ( in_array( $key, array( 'color', 'size', 'material', 'pattern', 'weight' ), true ) ) {
								$varies_by[] = $key;
							}
						}
					}
				}
			}
		}

		if ( ! empty( $variants ) ) {
			$schema['hasVariant'] = $variants;
		}

		// Add variesBy property.
		if ( ! empty( $varies_by ) ) {
			$valid_map = array(
				'color'    => 'https://schema.org/color',
				'size'     => 'https://schema.org/size',
				'material' => 'https://schema.org/material',
				'pattern'  => 'https://schema.org/pattern',
				'weight'   => 'https://schema.org/weight',
			);

			$varies_by_urls = array();
			foreach ( array_unique( $varies_by ) as $attr ) {
				if ( isset( $valid_map[ $attr ] ) ) {
					$varies_by_urls[] = $valid_map[ $attr ];
				}
			}

			if ( ! empty( $varies_by_urls ) ) {
				$schema['variesBy'] = $varies_by_urls;
			}
		}

		return $schema;
	}

	/**
	 * Build a single variant's schema data.
	 *
	 * @param \WC_Product_Variation $variation Variation object.
	 * @param \WC_Product           $product   Parent product object.
	 * @return array
	 */
	protected function build_variant_data( $variation, $product ) {
		if ( ! is_object( $variation ) ) {
			return array();
		}

		$variant = array(
			'@type' => 'Product',
			'name'  => method_exists( $variation, 'get_name' ) ? $variation->get_name() : '',
		);

		// SKU.
		if ( method_exists( $variation, 'get_sku' ) ) {
			$sku = $variation->get_sku();
			if ( ! empty( $sku ) ) {
				$variant['sku'] = $sku;
			}
		}

		// Description.
		$desc = '';
		if ( method_exists( $variation, 'get_description' ) && $variation->get_description() ) {
			$desc = $variation->get_description();
		} elseif ( method_exists( $product, 'get_short_description' ) && $product->get_short_description() ) {
			$desc = $product->get_short_description();
		} elseif ( method_exists( $product, 'get_description' ) ) {
			$desc = $product->get_description();
		}

		if ( ! empty( $desc ) ) {
			$variant['description'] = wp_strip_all_tags( $desc, true );
		}

		// Image.
		if ( method_exists( $variation, 'get_image_id' ) ) {
			$image_id = $variation->get_image_id();
			if ( $image_id ) {
				$image_url = wp_get_attachment_image_url( $image_id, 'full' );
				if ( $image_url ) {
					$variant['image'] = $image_url;
				}
			}
		}

		// Offer.
		if ( function_exists( 'wc_get_price_to_display' ) && method_exists( $variation, 'get_id' ) ) {
			$price = wc_get_price_to_display( $variation );

			$availability = 'https://schema.org/InStock';
			if ( method_exists( $variation, 'is_in_stock' ) && ! $variation->is_in_stock() ) {
				$availability = 'https://schema.org/OutOfStock';
			}

			$currency = '';
			if ( function_exists( 'get_woocommerce_currency' ) ) {
				$currency = get_woocommerce_currency();
			}

			$variant['offers'] = array(
				'@type'         => 'Offer',
				'price'         => $price,
				'priceCurrency' => $currency,
				'availability'  => $availability,
				'url'           => method_exists( $variation, 'get_permalink' ) ? $variation->get_permalink() : '',
			);
		}

		// GTIN / identifiers for the variation.
		if ( Options::is_wc_per_product_identifiers() && method_exists( $variation, 'get_id' ) ) {
			$pid    = Product_Identifiers::get_instance();
			$result = $pid->get_schema_identifiers( $variation->get_id() );

			if ( ! empty( $result['properties'] ) ) {
				foreach ( $result['properties'] as $type => $value ) {
					$variant[ $type ] = $value;
				}
			} else {
				// Fall back to single-type mode.
				$variation_gtin = $this->get_gtin_value( $variation );
				if ( ! empty( $variation_gtin ) ) {
					$gtin_type             = Options::get_wc_gtin_type();
					$variant[ $gtin_type ] = $variation_gtin;
				}
			}
		} else {
			$variation_gtin = $this->get_gtin_value( $variation );
			if ( ! empty( $variation_gtin ) ) {
				$gtin_type             = Options::get_wc_gtin_type();
				$variant[ $gtin_type ] = $variation_gtin;
			}
		}

		// Attributes (color, size, etc.).
		if ( method_exists( $variation, 'get_attributes' ) ) {
			$attributes = $variation->get_attributes();
			if ( is_array( $attributes ) ) {
				foreach ( $attributes as $key => $value ) {
					if ( '' === $value && '0' !== $value ) {
						continue;
					}
					$key = str_replace( 'pa_', '', $key );
					if ( in_array( $key, array( 'color', 'size', 'material', 'pattern', 'weight' ), true ) ) {
						$variant[ $key ] = $value;
					}
				}
			}
		}

		return $variant;
	}

	// =========================================================================
	// Open Graph Product Tags
	// =========================================================================

	/**
	 * Set Open Graph type to 'product' on product pages.
	 *
	 * @param string $og_type Current OG type.
	 * @return string
	 */
	public function product_og_type( $og_type ) {
		if ( is_singular( 'product' ) ) {
			return 'product';
		}

		return $og_type;
	}

	/**
	 * Output WooCommerce-specific Open Graph tags on product pages.
	 *
	 * @return void
	 */
	public function output_product_og_tags() {
		if ( ! is_singular( 'product' ) ) {
			return;
		}

		$product = $this->get_product();

		if ( ! is_object( $product ) ) {
			return;
		}

		echo "\n<!-- SEOvault WooCommerce Open Graph -->\n";

		// Brand (with custom brand override support).
		$brand = $this->get_product_brand_with_override( $product->get_id() );
		if ( ! empty( $brand ) ) {
			echo '<meta property="product:brand" content="' . esc_attr( $brand ) . '" />' . "\n";
		}

		// Price and currency (skip for variable products).
		if ( ! $product->is_type( 'variable' ) ) {
			$price = $product->get_price();

			if ( '' !== $price && null !== $price ) {
				echo '<meta property="product:price:amount" content="' . esc_attr( $price ) . '" />' . "\n";

				$currency = '';
				if ( function_exists( 'get_woocommerce_currency' ) ) {
					$currency = get_woocommerce_currency();
				}
				if ( ! empty( $currency ) ) {
					echo '<meta property="product:price:currency" content="' . esc_attr( $currency ) . '" />' . "\n";
				}
			}
		}

		// Availability.
		if ( $product->is_in_stock() ) {
			echo '<meta property="product:availability" content="instock" />' . "\n";
		} else {
			echo '<meta property="product:availability" content="oos" />' . "\n";
		}

		// Product gallery images as additional OG images.
		$this->output_gallery_og_images( $product );
	}

	/**
	 * Output product gallery images as OG image tags.
	 *
	 * @param \WC_Product $product Product object.
	 * @return void
	 */
	protected function output_gallery_og_images( $product ) {
		$img_ids = array();

		if ( method_exists( $product, 'get_gallery_image_ids' ) ) {
			$img_ids = $product->get_gallery_image_ids();
		} elseif ( method_exists( $product, 'get_gallery_attachment_ids' ) ) {
			$img_ids = $product->get_gallery_attachment_ids();
		}

		if ( ! is_array( $img_ids ) || empty( $img_ids ) ) {
			return;
		}

		foreach ( $img_ids as $img_id ) {
			$img_id = absint( $img_id );
			if ( ! $img_id ) {
				continue;
			}

			$url = wp_get_attachment_image_url( $img_id, 'full' );
			if ( $url ) {
				echo '<meta property="og:image" content="' . esc_url( $url ) . '" />' . "\n";
			}
		}
	}

	// =========================================================================
	// Product Schema
	// =========================================================================

	/**
	 * Add Product schema to the schema graph (legacy filter helper).
	 *
	 * Prefer ProductSchemaContributor for V2 tagged integration emission.
	 * Kept for direct callers/tests; no longer hooked to seovaultseo_set_schema.
	 *
	 * @param array         $schemas Existing schema data.
	 * @param \WP_Post|null $object  Explicit post during save-time validation.
	 * @return array
	 */
	public function add_product_schema( $schemas, $object = null ) {
		$contributor = new ProductSchemaContributor();
		foreach ( $contributor->collect_tagged_schemas( $object ) as $tagged ) {
			if ( isset( $tagged['schema'] ) && is_array( $tagged['schema'] ) && ! empty( $tagged['schema'] ) ) {
				$schemas[] = $tagged['schema'];
			}
		}

		return is_array( $schemas ) ? $schemas : array();
	}

	/**
	 * Build Product JSON-LD schema array.
	 *
	 * Re-entrant calls (e.g. filters/hooks that re-trigger product schema during
	 * an active build) return an empty array so nested work cannot recurse/OOM.
	 *
	 * @param int $post_id Product post ID.
	 * @return array
	 */
	public function build_product_schema( $post_id ) {
		$post_id = absint( $post_id );

		if ( ! $post_id ) {
			return array();
		}

		if ( self::$building_product_schema ) {
			return array();
		}

		self::$building_product_schema = true;

		try {
			return $this->do_build_product_schema( $post_id );
		} finally {
			self::$building_product_schema = false;
		}
	}

	/**
	 * Internal product schema builder (assumes re-entrancy lock is held).
	 *
	 * @param int $post_id Product post ID.
	 * @return array
	 */
	private function do_build_product_schema( $post_id ) {
		$product = $this->get_product_by_id( $post_id );

		if ( ! is_object( $product ) ) {
			return array();
		}

		$schema = array(
			'@type'       => 'Product',
			'@id'         => get_permalink( $post_id ) . '#product',
			'name'        => get_the_title( $post_id ),
			'url'         => get_permalink( $post_id ),
			'description' => $this->get_short_description( $product ),
		);

		// SKU.
		$sku = $product->get_sku();
		if ( ! empty( $sku ) ) {
			$schema['sku'] = $sku;
		}

		// Brand (with custom brand override support).
		$brand = $this->get_product_brand_with_override( $post_id );
		if ( ! empty( $brand ) ) {
			$schema['brand'] = array(
				'@type' => 'Brand',
				'name'  => $brand,
			);
		}

		// Price and availability.
		if ( ! $product->is_type( 'variable' ) ) {
			$price = $product->get_price();

			if ( '' !== $price && null !== $price ) {
				$schema['offers'] = array(
					'@type'         => 'Offer',
					'price'         => $price,
					'priceCurrency' => function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : '',
					'url'           => get_permalink( $post_id ),
					'availability'  => $product->is_in_stock() ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock',
				);
			}
		}

		// Image.
		$image_id = $product->get_image_id();
		if ( $image_id ) {
			$image_url = wp_get_attachment_image_url( $image_id, 'full' );
			if ( $image_url ) {
				$schema['image'] = $image_url;
			}
		}

		// Aggregate rating from reviews.
		if ( function_exists( 'wc_get_product' ) && method_exists( $product, 'get_rating_count' ) ) {
			$rating_count = $product->get_rating_count();
			$average      = $product->get_average_rating();

			if ( $rating_count > 0 && $average > 0 ) {
				$schema['aggregateRating'] = array(
					'@type'       => 'AggregateRating',
					'ratingValue' => $average,
					'reviewCount' => $rating_count,
				);
			}
		}

		// GTIN / MPN / ISBN — use configured identifier type, or per-product identifiers if enabled.
		if ( Options::is_wc_per_product_identifiers() ) {
			$pid    = Product_Identifiers::get_instance();
			$result = $pid->get_schema_identifiers( $post_id );

			if ( ! empty( $result['properties'] ) ) {
				unset( $schema['gtin'] );
				foreach ( $result['properties'] as $type => $value ) {
					$schema[ $type ] = $value;
				}
				if ( $result['has_isbn'] ) {
					$schema['@type'] = array( 'Product', 'Book' );
				}
			} else {
				// Fall back to single-type mode if no per-product data.
				$gtin_type = Options::get_wc_gtin_type();
				$gtin      = $this->get_gtin_value( $product );

				if ( ! empty( $gtin ) ) {
					unset( $schema['gtin'] );
					$schema[ $gtin_type ] = $gtin;
					if ( 'isbn' === $gtin_type ) {
						$schema['@type'] = array( 'Product', 'Book' );
					}
				} elseif ( method_exists( $product, 'get_global_unique_id' ) ) {
					$native_gtin = $product->get_global_unique_id();
					if ( ! empty( $native_gtin ) ) {
						$schema[ $gtin_type ] = $native_gtin;
					}
				}
			}
		} else {
			$gtin_type = Options::get_wc_gtin_type();
			$gtin      = $this->get_gtin_value( $product );

			if ( ! empty( $gtin ) ) {
				// Remove any default 'gtin' key so the configured type key is used.
				unset( $schema['gtin'] );
				$schema[ $gtin_type ] = $gtin;

				// If ISBN is the type, extend the schema @type to include Book.
				if ( 'isbn' === $gtin_type ) {
					$schema['@type'] = array( 'Product', 'Book' );
				}
			} elseif ( method_exists( $product, 'get_global_unique_id' ) ) {
				$native_gtin = $product->get_global_unique_id();
				if ( ! empty( $native_gtin ) ) {
					$schema[ $gtin_type ] = $native_gtin;
				}
			}
		}

		// Additional schema properties (manufacturer, brand URL) if enabled.
		if ( Options::is_wc_additional_schema() ) {
			$schema = $this->add_additional_schema_properties( $schema, $post_id );
		}

		// Multi-currency offers if configured.
		$extra_currencies = Options::get_wc_multicurrency_currencies();
		if ( ! empty( $extra_currencies ) && isset( $schema['offers'] ) ) {
			$schema['offers'] = $this->add_multicurrency_offers( $schema['offers'], $extra_currencies );
		}

		// Variable product: add ProductGroup variant data.
		if ( $product->is_type( 'variable' ) ) {
			$schema = $this->add_variable_product_schema( $schema, $product, $post_id );
		}

		return apply_filters( 'seovaultseo_wc_product_schema', $schema, $post_id, $product );
	}

	// =========================================================================
	// Meta Description Fallback
	// =========================================================================

	/**
	 * Use product short/long description as meta description fallback.
	 *
	 * @param string $description Current description.
	 * @param int    $post_id     Post ID (optional, from filter context).
	 * @return string
	 */
	public function product_meta_description( $description = '', $post_id = 0 ) {
		if ( ! empty( $description ) ) {
			return $description;
		}

		if ( ! is_singular( 'product' ) ) {
			return $description;
		}

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

		$post_id = absint( $post_id );

		if ( ! $post_id ) {
			return '';
		}

		$product = $this->get_product_by_id( $post_id );

		if ( ! is_object( $product ) ) {
			return '';
		}

		$short_desc = $this->get_short_description( $product );

		if ( ! empty( $short_desc ) ) {
			return $short_desc;
		}

		$long_desc = $this->get_long_description( $product );

		if ( ! empty( $long_desc ) ) {
			return $this->truncate( $long_desc, 156 );
		}

		return '';
	}

	/**
	 * Truncate a string to a given length.
	 *
	 * @param string $string String to truncate.
	 * @param int    $length Max character length.
	 * @return string
	 */
	protected function truncate( $string, $length = 156 ) {
		$string = trim( $string );

		if ( mb_strlen( $string ) <= $length ) {
			return $string;
		}

		$truncated = mb_substr( $string, 0, $length );

		// Cut at last word boundary.
		$last_space = mb_strrpos( $truncated, ' ' );

		if ( false !== $last_space ) {
			$truncated = mb_substr( $truncated, 0, $last_space );
		}

		return $truncated;
	}

	// =========================================================================
	// Robots for Cart / Checkout / Account
	// =========================================================================

	/**
	 * Set noindex for cart, checkout, and account pages.
	 *
	 * @param string $robots Current robots meta content.
	 * @return string
	 */
	public function cart_checkout_robots( $robots ) {
		if ( ! function_exists( 'is_cart' ) || ! function_exists( 'is_checkout' ) || ! function_exists( 'is_account_page' ) ) {
			return $robots;
		}

		if ( is_cart() || is_checkout() || is_account_page() ) {
			// Remove WC's own noindex so we can control it.
			remove_action( 'wp_head', 'wc_page_noindex' );

			return 'noindex, follow';
		}

		return $robots;
	}

	// =========================================================================
	// Sitemap Exclusions
	// =========================================================================

	/**
	 * Exclude product variations and shop coupons from the sitemap.
	 *
	 * @param array  $args      Query args.
	 * @param string $post_type Post type slug.
	 * @return array
	 */
	public function exclude_wc_post_types( $args, $post_type ) {
		$exclude = array( 'product_variation', 'shop_coupon' );

		if ( in_array( $post_type, $exclude, true ) ) {
			// phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Intentional exclusion of WC internal post types.
			$args['post__not_in'] = array( -1 );
		}

		return $args;
	}

	/**
	 * Exclude WooCommerce internal taxonomies from the sitemap.
	 *
	 * @param array  $args      Query args.
	 * @param string $taxonomy  Taxonomy slug.
	 * @return array
	 */
	public function exclude_wc_taxonomies( $args, $taxonomy ) {
		$exclude = array( 'product_type', 'product_shipping_class', 'shop_order_status' );

		if ( in_array( $taxonomy, $exclude, true ) ) {
			$args['hide_empty'] = true;
			$args['number']     = 0;
		}

		// Exclude product attribute taxonomies (pa_*).
		if ( 0 === strpos( $taxonomy, 'pa_' ) ) {
			$args['hide_empty'] = true;
			$args['number']     = 0;
		}

		return $args;
	}

	// =========================================================================
	// Permalink Optimization
	// =========================================================================

	/**
	 * Initialize permalink optimization if enabled.
	 *
	 * @return void
	 */
	protected function init_permalink_optimization() {
		$remove_product_base  = Options::is_wc_remove_product_base();
		$remove_category_base = Options::is_wc_remove_category_base();
		$remove_parent_slugs  = Options::is_wc_remove_parent_slugs();

		if ( ! $remove_product_base && ! $remove_category_base && ! $remove_parent_slugs ) {
			return;
		}

		// Remove product base from permalinks.
		if ( $remove_product_base ) {
			add_filter( 'post_type_link', array( $this, 'filter_product_permalink' ), 1, 2 );
			add_filter( 'request', array( $this, 'filter_request' ), 11 );
		}

		// Remove category base / parent slugs.
		if ( $remove_category_base || $remove_parent_slugs ) {
			add_action( 'created_product_cat', array( $this, 'flush_rewrite_rules' ) );
			add_action( 'delete_product_cat', array( $this, 'flush_rewrite_rules' ) );
			add_action( 'edited_product_cat', array( $this, 'flush_rewrite_rules' ) );

			add_filter( 'term_link', array( $this, 'filter_category_permalink' ), 0, 3 );
			add_filter( 'rewrite_rules_array', array( $this, 'add_rewrite_rules' ), 99 );
		}
	}

	/**
	 * Filter product permalink to remove the product base.
	 *
	 * @param string   $permalink The permalink URL.
	 * @param \WP_Post $post     Post object.
	 * @return string
	 */
	public function filter_product_permalink( $permalink, $post ) {
		if ( 'product' !== $post->post_type ) {
			return $permalink;
		}

		if ( ! get_option( 'permalink_structure' ) ) {
			return $permalink;
		}

		$product_base = $this->get_product_base();

		if ( ! empty( $product_base ) ) {
			$permalink = str_replace( $product_base, '/', $permalink );
		}

		return $permalink;
	}

	/**
	 * Filter the request to find products without the base.
	 *
	 * @param array $request Current request.
	 * @return array
	 */
	public function filter_request( $request ) {
		global $wp, $wpdb;

		$url = $wp->request;

		if ( empty( $url ) ) {
			return $request;
		}

		$replace = array();
		$url     = explode( '/', $url );
		$slug    = array_pop( $url );

		if ( 'feed' === $slug ) {
			$replace['feed'] = $slug;
			$slug            = array_pop( $url );
		}

		if ( ! empty( $slug ) && 0 === strpos( $slug, 'comment-page-' ) ) {
			$replace['cpage'] = substr( $slug, strlen( 'comment-page-' ) );
			$slug             = array_pop( $url );
		}

		$slug = sanitize_title( $slug );

		if ( empty( $slug ) ) {
			return $request;
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$count = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_name = %s AND post_type = %s",
				$slug,
				'product'
			)
		);

		if ( $count > 0 ) {
			$replace['page']      = '';
			$replace['name']      = $slug;
			$replace['product']   = $slug;
			$replace['post_type'] = 'product';

			return $replace;
		}

		return $request;
	}

	/**
	 * Filter product category permalink.
	 *
	 * @param string   $link     Term link URL.
	 * @param \WP_Term $term    Term object.
	 * @param string   $taxonomy Taxonomy slug.
	 * @return string
	 */
	public function filter_category_permalink( $link, $term, $taxonomy ) {
		if ( 'product_cat' !== $taxonomy ) {
			return $link;
		}

		if ( ! get_option( 'permalink_structure' ) ) {
			return $link;
		}

		$remove_category_base = Options::is_wc_remove_category_base();
		$remove_parent_slugs  = Options::is_wc_remove_parent_slugs();

		if ( ! $remove_category_base && ! $remove_parent_slugs ) {
			return $link;
		}

		if ( ! function_exists( 'wc_get_permalink_structure' ) ) {
			return $link;
		}

		$permalink_structure = wc_get_permalink_structure();
		$category_base       = trailingslashit( $permalink_structure['category_rewrite_slug'] );

		if ( $remove_category_base ) {
			$link          = str_replace( $category_base, '', $link );
			$category_base = '';
		}

		if ( $remove_parent_slugs ) {
			$link = home_url( user_trailingslashit( $category_base . $term->slug ) );
		}

		return $link;
	}

	/**
	 * Add rewrite rules for clean product/category URLs.
	 *
	 * @param array $rules Existing rewrite rules.
	 * @return array
	 */
	public function add_rewrite_rules( $rules ) {
		global $wp_rewrite;

		if ( ! function_exists( 'wc_get_permalink_structure' ) ) {
			return $rules;
		}

		$remove_category_base = Options::is_wc_remove_category_base();
		$remove_parent_slugs  = Options::is_wc_remove_parent_slugs();

		$permalink_structure = wc_get_permalink_structure();
		$category_base       = $remove_category_base ? '' : $permalink_structure['category_rewrite_slug'];

		$categories = $this->get_product_categories();
		$feed       = '(' . trim( implode( '|', $wp_rewrite->feeds ) ) . ')';

		$category_rules = array();
		$product_rules  = array();

		foreach ( $categories as $category ) {
			$cat_path = $this->get_category_fullpath( $category, $categories );
			$cat_slug = $category_base . ( $remove_parent_slugs ? $category['slug'] : $cat_path );
			$cat_slug = urldecode( $cat_slug );

			$category_rules[ $cat_slug . '/?$' ]       = 'index.php?product_cat=' . $category['slug'];
			$category_rules[ $cat_slug . '/embed/?$' ] = 'index.php?product_cat=' . $category['slug'] . '&embed=true';
			$category_rules[ $cat_slug . '/' . $wp_rewrite->feed_base . '/' . $feed . '/?$' ]      = 'index.php?product_cat=' . $category['slug'] . '&feed=$matches[1]';
			$category_rules[ $cat_slug . '/' . $feed . '/?$' ]                                     = 'index.php?product_cat=' . $category['slug'] . '&feed=$matches[1]';
			$category_rules[ $cat_slug . '/' . $wp_rewrite->pagination_base . '/?([0-9]{1,})/?$' ] = 'index.php?product_cat=' . $category['slug'] . '&paged=$matches[1]';

			if ( Options::is_wc_remove_product_base() ) {
				$cat_path_decoded                                   = urldecode( $cat_path );
				$product_rules[ $cat_path_decoded . '/([^/]+)/?$' ] = 'index.php?product=$matches[1]';
			}
		}

		$rules = empty( $rules ) ? array() : $rules;

		return $category_rules + $product_rules + $rules;
	}

	/**
	 * Flush rewrite rules.
	 *
	 * @return void
	 */
	public function flush_rewrite_rules() {
		flush_rewrite_rules( false );
	}

	/**
	 * Get product categories as an associative array.
	 *
	 * @return array
	 */
	protected function get_product_categories() {
		$categories = get_categories(
			array(
				'taxonomy'   => 'product_cat',
				'hide_empty' => false,
			)
		);

		$slugs = array();
		foreach ( $categories as $category ) {
			$slugs[ $category->term_id ] = array(
				'parent' => $category->parent,
				'slug'   => $category->slug,
			);
		}

		return $slugs;
	}

	/**
	 * Recursively build a category's full path.
	 *
	 * @param array $category   Category data.
	 * @param array $categories All categories.
	 * @return string
	 */
	protected function get_category_fullpath( $category, $categories ) {
		$parent = $category['parent'];

		if ( $parent > 0 && isset( $categories[ $parent ] ) ) {
			return $this->get_category_fullpath( $categories[ $parent ], $categories ) . '/' . $category['slug'];
		}

		return $category['slug'];
	}

	/**
	 * Get the product base from WC permalink structure.
	 *
	 * @return string
	 */
	protected function get_product_base() {
		if ( ! function_exists( 'wc_get_permalink_structure' ) ) {
			return '';
		}

		$permalink_structure = wc_get_permalink_structure();
		$product_base        = $permalink_structure['product_rewrite_slug'];

		if ( false !== strpos( $product_base, '%product_cat%' ) ) {
			$product_base = str_replace( '%product_cat%', '', $product_base );
		}

		$product_base = trim( $product_base, '/' );

		if ( empty( $product_base ) ) {
			return '';
		}

		return '/' . $product_base . '/';
	}
}
