<?php

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

class AIEO_SEO_Head_Builder {

	
	private static function is_post_type( $object_type ) {
		return post_type_exists( $object_type );
	}

	
	
	

	public static function register_build_hooks() {
		add_action( 'save_post', array( __CLASS__, 'on_save_post' ), 20, 1 );
		add_action( 'edited_term', array( __CLASS__, 'on_edited_term' ), 20, 3 );
		add_action( 'woocommerce_update_product', array( __CLASS__, 'on_update_product' ), 20, 1 );
		
		
		
		add_action( 'set_object_terms', array( __CLASS__, 'on_set_object_terms' ), 10, 4 );
		
		
		
		
		add_action( 'before_delete_post', array( __CLASS__, 'on_delete_post' ), 20, 1 );
		add_action( 'delete_term', array( __CLASS__, 'on_delete_term' ), 20, 3 );
		
		add_action( 'aieo_seo_async_drain', array( __CLASS__, 'run_async_drain' ) );
	}

	
	private static function should_build_inline() {
		
		
		
		
		return wp_doing_cron() || ( defined( 'WP_CLI' ) && WP_CLI );
	}

	
	private static $async_drain_hooked = false;

	
	private static function maybe_mark_dirty( $object_type, $object_id ) {
		if ( ! class_exists( 'AIEO_SEO_Store' ) || ! AIEO_SEO_Store::is_active_provider() ) {
			return;
		}
		self::mark_dirty( $object_type, $object_id );
		if ( ! self::$async_drain_hooked ) {
			self::$async_drain_hooked = true;
			add_action( 'shutdown', array( __CLASS__, 'async_build_after_response' ), 99 );
		}
	}

	
	public static function async_build_after_response() {
		if ( function_exists( 'fastcgi_finish_request' ) ) {
			@fastcgi_finish_request(); 
			self::drain_dirty( 200 );
		} elseif ( ! wp_next_scheduled( 'aieo_seo_async_drain' ) ) {
			wp_schedule_single_event( time() + 1, 'aieo_seo_async_drain' );
		}
	}

	
	public static function run_async_drain() {
		self::drain_dirty( 200 );
	}

	public static function on_save_post( $post_id ) {
		$post_id = (int) $post_id;
		if ( $post_id <= 0 || wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
			return;
		}
		$type = (string) get_post_type( $post_id );
		
		
		
		
		if ( '' === $type || ! self::is_public_content_type( $type ) ) {
			return;
		}
		if ( self::should_build_inline() ) {
			self::build( $type, $post_id );
		} else {
			self::maybe_mark_dirty( $type, $post_id );
		}
	}

	public static function on_edited_term( $term_id, $tt_id = 0, $taxonomy = '' ) {
		$term_id  = (int) $term_id;
		$taxonomy = (string) $taxonomy;
		if ( $term_id <= 0 || '' === $taxonomy || ! is_taxonomy_viewable( $taxonomy ) ) {
			return;
		}
		if ( self::should_build_inline() ) {
			self::build( $taxonomy, $term_id );
		} else {
			self::maybe_mark_dirty( $taxonomy, $term_id );
		}
	}

	public static function on_update_product( $product_id ) {
		$product_id = (int) $product_id;
		if ( $product_id <= 0 ) {
			return;
		}
		if ( self::should_build_inline() ) {
			self::build( 'product', $product_id );
		} else {
			self::maybe_mark_dirty( 'product', $product_id );
		}
	}

	
	public static function on_set_object_terms( $object_id, $terms, $tt_ids, $taxonomy ) {
		if ( 'product_cat' !== $taxonomy && 'category' !== $taxonomy ) {
			return;
		}
		if ( ! class_exists( 'AIEO_SEO_Store' ) ) {
			return;
		}
		$object_id = (int) $object_id;
		$type      = (string) get_post_type( $object_id );
		if ( '' === $type ) {
			return;
		}
		$pinned = (int) AIEO_SEO_Store::get_primary_cat_id( $type, $object_id );
		if ( $pinned > 0 && ! has_term( $pinned, $taxonomy, $object_id ) ) {
			AIEO_SEO_Store::set_primary_term( $type, $object_id, $taxonomy, 0 ); 
			self::maybe_mark_dirty( $type, $object_id );                          
		}
	}

	public static function on_delete_post( $post_id ) {
		$post_id = (int) $post_id;
		if ( $post_id <= 0 || wp_is_post_revision( $post_id ) ) {
			return;
		}
		$type = (string) get_post_type( $post_id );
		if ( '' !== $type ) {
			self::delete_all( $type, $post_id );
		}
	}

	public static function on_delete_term( $term_id, $tt_id = 0, $taxonomy = '' ) {
		$term_id  = (int) $term_id;
		$taxonomy = (string) $taxonomy;
		if ( $term_id > 0 && '' !== $taxonomy ) {
			self::delete_all( $taxonomy, $term_id );
		}
	}

	
	
	

	
	public static function build( $object_type, $object_id, $lang = null ) {
		$object_type = (string) $object_type;
		$object_id   = (int) $object_id;
		if ( '' === $object_type || $object_id <= 0 ) {
			return false;
		}
		if ( null === $lang ) {
			$lang = class_exists( 'AIEO_SEO_Head' ) ? AIEO_SEO_Head::current_lang() : '';
		}

		
		
		
		static $built = array();
		$memo = $object_type . ':' . $object_id . ':' . $lang;
		if ( isset( $built[ $memo ] ) ) {
			return true;
		}
		$built[ $memo ] = true;

		
		
		
		
		
		
		if ( ! class_exists( 'AIEO_SEO_Store' ) || ! AIEO_SEO_Store::is_active_provider() ) {
			return true;
		}

		
		
		
		
		
		
		if ( class_exists( 'AIEO_SEO_Primary_Term' ) ) {
			AIEO_SEO_Primary_Term::bust_cache( $object_type, $object_id );
		}

		
		
		
		
		
		
		
		if ( class_exists( 'AIEO_SEO_Primary_Term' ) && method_exists( 'AIEO_SEO_Primary_Term', 'ensure_deepest_primary' ) ) {
			AIEO_SEO_Primary_Term::ensure_deepest_primary( $object_type, $object_id );
		}

		$ctx = self::resolve( $object_type, $object_id );
		if ( ! $ctx || empty( $ctx['materialise'] ) ) {
			
			
			
			
			self::delete_row( $object_type, $object_id, $lang );
			return true;
		}

		$override = ( class_exists( 'AIEO_SEO_Store' ) ) ? AIEO_SEO_Store::get( $object_type, $object_id ) : null;
		$override = is_array( $override ) ? $override : array();

		
		
		
		
		if ( 'term' === $ctx['kind'] && empty( $override['canonical_override'] ) && self::is_ugly_term_url( (string) $ctx['url'] ) ) {
			return true;
		}

		$sep   = (string) ( class_exists( 'AIEO_DMM_Settings' ) ? AIEO_DMM_Settings::get( 'seo.separator', '-' ) : '-' );
		$site  = get_bloginfo( 'name' );

		
		
		$title_fmt     = self::type_format( $object_type, $ctx['kind'], 'title' );
		$title_default = ( '' !== $title_fmt ) ? self::expand_tokens( $title_fmt, $ctx ) : trim( $ctx['name'] . ' ' . $sep . ' ' . $site );
		$title         = self::resolve_field( $override, 'title', $ctx, $title_default );

		$desc_fmt     = self::type_format( $object_type, $ctx['kind'], 'description' );
		$desc_default = ( '' !== $desc_fmt ) ? self::truncate( wp_strip_all_tags( self::expand_tokens( $desc_fmt, $ctx ) ), 160 ) : self::default_description( $ctx );
		$desc         = self::resolve_field( $override, 'description', $ctx, $desc_default );
		
		
		
		
		
		
		$canon  = ! empty( $override['canonical_override'] ) ? (string) $override['canonical_override'] : $ctx['url'];
		$robots = self::compute_robots( $override, $ctx );
		
		
		
		
		
		$indexable = ( ! empty( $ctx['indexable'] ) && false === strpos( (string) $robots, 'noindex' ) ) ? 1 : 0;
		self::materialise_canonical( $object_type, $object_id, $canon, $override, $indexable );
		$og       = self::build_og( $override, $ctx, $title, $desc, $canon, $site );
		$twitter  = self::build_twitter( $override, $ctx, $title, $desc, $og );
		$graph    = self::build_jsonld( $override, $ctx, $title, $desc, $canon, $site );

		$head_html = self::assemble_head_html( $canon, $og, $twitter, $graph );
		$hash      = sha1( $title . '|' . $robots . '|' . $head_html );

		self::write_row( $object_type, $object_id, $lang, $title, $canon, $robots, $head_html, $hash );
		return true;
	}

	
	public static function preview( $object_type, $object_id, $lang = null ) {
		$object_type = (string) $object_type;
		$object_id   = (int) $object_id;
		if ( '' === $object_type || $object_id <= 0 ) {
			return null;
		}
		if ( class_exists( 'AIEO_SEO_Primary_Term' ) ) {
			AIEO_SEO_Primary_Term::bust_cache( $object_type, $object_id );
		}
		$ctx = self::resolve( $object_type, $object_id );
		if ( ! $ctx ) {
			return null;
		}
		$override = class_exists( 'AIEO_SEO_Store' ) ? AIEO_SEO_Store::get( $object_type, $object_id ) : null;
		$override = is_array( $override ) ? $override : array();

		$sep  = (string) ( class_exists( 'AIEO_DMM_Settings' ) ? AIEO_DMM_Settings::get( 'seo.separator', '-' ) : '-' );
		$site = get_bloginfo( 'name' );

		$title_fmt     = self::type_format( $object_type, $ctx['kind'], 'title' );
		$title_default = ( '' !== $title_fmt ) ? self::expand_tokens( $title_fmt, $ctx ) : trim( $ctx['name'] . ' ' . $sep . ' ' . $site );
		$title         = self::resolve_field( $override, 'title', $ctx, $title_default );

		$desc_fmt     = self::type_format( $object_type, $ctx['kind'], 'description' );
		$desc_default = ( '' !== $desc_fmt ) ? self::truncate( wp_strip_all_tags( self::expand_tokens( $desc_fmt, $ctx ) ), 160 ) : self::default_description( $ctx );
		$desc         = self::resolve_field( $override, 'description', $ctx, $desc_default );

		$canon  = ! empty( $override['canonical_override'] ) ? (string) $override['canonical_override'] : (string) $ctx['url'];
		$robots = self::compute_robots( $override, $ctx );
		$graph  = self::build_jsonld( $override, $ctx, $title, $desc, $canon, $site );

		$types = array();
		if ( preg_match_all( '#"@type":(?:"([^"]+)"|\[([^\]]+)\])#', (string) wp_json_encode( $graph ), $mm, PREG_SET_ORDER ) ) {
			foreach ( $mm as $m ) {
				$types[] = ( isset( $m[1] ) && '' !== $m[1] ) ? $m[1] : trim( str_replace( array( '"', ' ' ), '', $m[2] ) );
			}
		}

		return array(
			'object_type'  => $object_type,
			'object_id'    => $object_id,
			'name'         => (string) $ctx['name'],
			'indexable'    => ! empty( $ctx['indexable'] ),
			'title'        => (string) $title,
			'description'  => (string) $desc,
			'canonical'    => (string) $canon,
			'permalink'    => (string) $ctx['url'],
			'robots'       => (string) $robots,
			'schema_types' => array_values( array_unique( array_filter( $types ) ) ),
		);
	}

	
	
	

	
	private static function resolve( $object_type, $object_id ) {
		if ( self::is_post_type( $object_type ) ) {
			$post = get_post( $object_id );
			if ( ! $post instanceof WP_Post ) {
				return null;
			}
			$published = ( 'publish' === $post->post_status );
			$indexable = $published;
			if ( 'product' === $object_type && function_exists( 'wc_get_product' ) ) {
				$product = wc_get_product( $object_id );
				if ( $product && 'hidden' === $product->get_catalog_visibility() ) {
					$indexable = false; 
				}
			}
			return array(
				'kind'        => 'post',
				'name'        => get_the_title( $post ),
				'url'         => (string) get_permalink( $post ),
				'indexable'   => $indexable,
				'materialise' => $published, 
				'post'        => $post,
				'term'        => null,
				'object_type' => $object_type,
				'object_id'   => $object_id,
			);
		}

		if ( taxonomy_exists( $object_type ) ) {
			$term = get_term( $object_id, $object_type );
			if ( ! $term instanceof WP_Term ) {
				return null;
			}
			$link  = get_term_link( $term );
			$valid = ! is_wp_error( $link );
			$indexable = $valid;
			if ( $valid ) {
				
				if ( isset( $term->count ) && 0 === (int) $term->count
					&& ( ! class_exists( 'AIEO_DMM_Settings' ) || AIEO_DMM_Settings::get( 'seo.noindex_empty_term', true ) ) ) {
					$indexable = false;
				}
				
				if ( 'hidden' === self::term_archive_template( $object_type, (int) $object_id ) ) {
					$indexable = false;
				}
			}
			return array(
				'kind'        => 'term',
				'name'        => $term->name,
				'url'         => $valid ? (string) $link : '',
				'indexable'   => $indexable,
				'materialise' => $valid, 
				'post'        => null,
				'term'        => $term,
				'object_type' => $object_type,
				'object_id'   => $object_id,
			);
		}

		return null;
	}

	
	private static function is_ugly_term_url( $url ) {
		if ( '' === (string) get_option( 'permalink_structure' ) ) {
			return false; 
		}
		return (bool) preg_match( '#[?&][a-z0-9_]+=#i', (string) $url );
	}

	
	
	

	
	private static function resolve_field( $override, $key, $ctx, $computed_default ) {
		if ( ! empty( $override[ $key ] ) ) {
			$val = (string) $override[ $key ];
			return ( false !== strpos( $val, '#' ) ) ? self::expand_tokens( $val, $ctx ) : $val;
		}
		return $computed_default;
	}

	
	private static function type_format( $object_type, $kind, $which ) {
		if ( ! class_exists( 'AIEO_DMM_Settings' ) ) {
			return '';
		}
		$group = ( 'term' === $kind ) ? 'taxonomies' : 'post_types';
		$key   = ( 'title' === $which ) ? 'title' : 'description';
		$v     = AIEO_DMM_Settings::get( 'seo.' . $group . '.' . $object_type . '.' . $key, '' );
		return is_string( $v ) ? $v : '';
	}

	
	private static function materialise_canonical( $object_type, $object_id, $absolute, $override, $indexable = 1 ) {
		$absolute = (string) $absolute;
		if ( '' === $absolute ) {
			$path = '';
		} else {
			
			
			
			
			
			
			$host      = (string) wp_parse_url( $absolute, PHP_URL_HOST );
			$site_host = (string) wp_parse_url( home_url(), PHP_URL_HOST );
			$path      = ( '' === $host || strtolower( $host ) === strtolower( $site_host ) )
				? (string) wp_make_link_relative( $absolute )
				: $absolute;
		}
		$indexable   = (int) $indexable ? 1 : 0;
		$current_url = isset( $override['canonical_url'] ) ? (string) $override['canonical_url'] : '';
		$current_idx = isset( $override['indexable'] ) ? (int) $override['indexable'] : 1;
		if ( $path === $current_url && $indexable === $current_idx ) {
			return; 
		}
		if ( class_exists( 'AIEO_SEO_Store' ) ) {
			AIEO_SEO_Store::save( $object_type, $object_id, array( 'canonical_url' => $path, 'indexable' => $indexable ) );
		}
	}

	
	private static function term_archive_template( $taxonomy, $term_id ) {
		$map = array(
			'product_cat'   => 'AIEO_DMM_Category_Extras',
			'product_brand' => 'AIEO_DMM_Brand_Extras',
			'pa_need'       => 'AIEO_DMM_Need_Extras',
		);
		if ( isset( $map[ $taxonomy ] ) && class_exists( $map[ $taxonomy ] ) && method_exists( $map[ $taxonomy ], 'get_template' ) ) {
			return (string) call_user_func( array( $map[ $taxonomy ], 'get_template' ), $taxonomy, (int) $term_id );
		}
		return 'default';
	}

	private static function default_description( $ctx ) {
		$raw = '';
		if ( 'post' === $ctx['kind'] && $ctx['post'] ) {
			$raw = $ctx['post']->post_excerpt;
			if ( '' === trim( (string) $raw ) ) {
				if ( 'product' === $ctx['object_type'] && function_exists( 'wc_get_product' ) ) {
					$product = wc_get_product( $ctx['object_id'] );
					$raw     = $product ? $product->get_short_description() : '';
				}
				if ( '' === trim( (string) $raw ) ) {
					$raw = $ctx['post']->post_content;
				}
			}
		} elseif ( 'term' === $ctx['kind'] && $ctx['term'] ) {
			$raw = term_description( $ctx['term']->term_id, $ctx['term']->taxonomy );
		}
		return self::truncate( wp_strip_all_tags( (string) $raw ), 160 );
	}

	private static function truncate( $text, $limit ) {
		$text = trim( preg_replace( '/\s+/u', ' ', (string) $text ) );
		if ( function_exists( 'mb_strlen' ) ) {
			if ( mb_strlen( $text ) <= $limit ) {
				return $text;
			}
			$cut = mb_substr( $text, 0, $limit );
			$sp  = mb_strrpos( $cut, ' ' );
			return ( false !== $sp ? mb_substr( $cut, 0, $sp ) : $cut ) . '…';
		}
		return ( strlen( $text ) <= $limit ) ? $text : rtrim( substr( $text, 0, $limit ) ) . '…';
	}

	private static function compute_robots( $override, $ctx ) {
		
		
		
		
		
		
		
		
		$explicit = isset( $override['robots_default'] ) && ! $override['robots_default'];

		$noindex  = $explicit ? ! empty( $override['robots_noindex'] ) : false;
		$nofollow = $explicit ? ! empty( $override['robots_nofollow'] ) : false;

		
		if ( empty( $ctx['indexable'] ) ) {
			$noindex = true; 
		}
		if ( 'term' === $ctx['kind'] && isset( $ctx['term']->count ) && 0 === (int) $ctx['term']->count
			&& ( ! class_exists( 'AIEO_DMM_Settings' ) || AIEO_DMM_Settings::get( 'seo.noindex_empty_term', true ) ) ) {
			$noindex = true; 
		}
		if ( 'page' === $ctx['object_type'] && self::is_wc_functional_page( (int) $ctx['object_id'] ) ) {
			$noindex = true; 
		}
		
		
		
		
		if ( ! $noindex && ! $explicit && class_exists( 'AIEO_DMM_Settings' ) ) {
			$group = ( 'term' === $ctx['kind'] ) ? 'taxonomies' : 'post_types';
			if ( AIEO_DMM_Settings::get( 'seo.' . $group . '.' . $ctx['object_type'] . '.noindex', false ) ) {
				$noindex = true;
			}
		}

		$nosnippet = $explicit && ! empty( $override['robots_nosnippet'] );

		$parts   = array();
		$parts[] = $noindex ? 'noindex' : 'index';
		$parts[] = $nofollow ? 'nofollow' : 'follow';
		if ( $explicit && ! empty( $override['robots_noarchive'] ) ) { $parts[] = 'noarchive'; }
		if ( $explicit && ! empty( $override['robots_noimageindex'] ) ) { $parts[] = 'noimageindex'; }
		if ( $nosnippet ) { $parts[] = 'nosnippet'; }

		
		if ( ! $nosnippet ) {
			$mi = ( $explicit && isset( $override['robots_max_imagepreview'] ) && '' !== $override['robots_max_imagepreview'] )
				? (string) $override['robots_max_imagepreview'] : 'large';
			$parts[] = 'max-image-preview:' . $mi;
			$ms = ( $explicit && isset( $override['robots_max_snippet'] ) && null !== $override['robots_max_snippet'] && '' !== $override['robots_max_snippet'] )
				? (int) $override['robots_max_snippet'] : -1;
			$parts[] = 'max-snippet:' . $ms;
			$mv = ( $explicit && isset( $override['robots_max_videopreview'] ) && null !== $override['robots_max_videopreview'] && '' !== $override['robots_max_videopreview'] )
				? (int) $override['robots_max_videopreview'] : -1;
			$parts[] = 'max-video-preview:' . $mv;
		}
		return implode( ', ', $parts );
	}

	
	private static function is_wc_functional_page( $post_id ) {
		if ( $post_id <= 0 || ! function_exists( 'wc_get_page_id' ) ) {
			return false;
		}
		foreach ( array( 'cart', 'checkout', 'myaccount' ) as $slug ) {
			if ( (int) wc_get_page_id( $slug ) === $post_id ) {
				return true;
			}
		}
		return false;
	}

	

	private static function image_for( $override, $ctx ) {
		
		
		
		if ( ! empty( $override['og_image_custom_url'] )
			&& in_array( (string) ( $override['og_image_type'] ?? '' ), array( 'custom', 'custom_image' ), true ) ) {
			return (string) $override['og_image_custom_url'];
		}
		if ( ! empty( $override['og_image_url'] ) ) {
			return (string) $override['og_image_url'];
		}
		if ( 'post' === $ctx['kind'] && has_post_thumbnail( $ctx['object_id'] ) ) {
			$src = wp_get_attachment_image_src( get_post_thumbnail_id( $ctx['object_id'] ), 'large' );
			if ( $src ) {
				return (string) $src[0];
			}
		}
		return '';
	}

	private static function build_og( $override, $ctx, $title, $desc, $canon, $site ) {
		$type = 'website';
		if ( 'product' === $ctx['object_type'] ) {
			$type = 'product';
		} elseif ( 'post' === $ctx['kind'] && 'page' !== $ctx['object_type'] ) {
			$type = 'article';
		}
		$og = array(
			'og:locale'      => get_locale(),
			'og:type'        => $type,
			'og:title'       => '' !== (string) ( $override['og_title'] ?? '' ) ? (string) $override['og_title'] : $title,
			'og:description' => '' !== (string) ( $override['og_description'] ?? '' ) ? (string) $override['og_description'] : $desc,
			'og:url'         => $canon,
			'og:site_name'   => $site,
		);
		$img = self::image_for( $override, $ctx );
		if ( '' === $img && class_exists( 'AIEO_DMM_Settings' ) ) {
			
			$img = (string) AIEO_DMM_Settings::get( 'seo.og_default_image', '' );
		}
		if ( '' !== $img ) {
			$og['og:image'] = $img;
		}
		return array_filter( $og, static function ( $v ) { return '' !== (string) $v; } );
	}

	private static function build_twitter( $override, $ctx, $title, $desc, $og ) {
		
		
		$img = '';
		if ( empty( $override['twitter_use_og'] )
			&& ! empty( $override['twitter_image_custom_url'] )
			&& in_array( (string) ( $override['twitter_image_type'] ?? '' ), array( 'custom', 'custom_image' ), true ) ) {
			$img = (string) $override['twitter_image_custom_url'];
		} elseif ( ! empty( $og['og:image'] ) ) {
			$img = (string) $og['og:image'];
		}
		$has_img = ( '' !== $img );
		$tw      = array(
			'twitter:card'        => $has_img ? 'summary_large_image' : 'summary',
			'twitter:title'       => '' !== (string) ( $override['twitter_title'] ?? '' ) ? (string) $override['twitter_title'] : ( $og['og:title'] ?? $title ),
			'twitter:description' => '' !== (string) ( $override['twitter_description'] ?? '' ) ? (string) $override['twitter_description'] : ( $og['og:description'] ?? $desc ),
		);
		if ( $has_img ) {
			$tw['twitter:image'] = $img;
		}
		$handle = class_exists( 'AIEO_DMM_Settings' ) ? (string) AIEO_DMM_Settings::get( 'seo.twitter_user', '' ) : '';
		if ( '' !== $handle ) {
			$tw['twitter:site'] = '@' . ltrim( $handle, '@' );
		}
		return array_filter( $tw, static function ( $v ) { return '' !== (string) $v; } );
	}

	

	private static function build_jsonld( $override, $ctx, $title, $desc, $canon, $site ) {
		$home  = trailingslashit( home_url() );
		$org_id = $home . '#organization';
		$site_id = $home . '#website';

		$graph = array();

		
		$represents = class_exists( 'AIEO_DMM_Settings' ) ? (string) AIEO_DMM_Settings::get( 'seo.org_represents', 'organization' ) : 'organization';
		$org        = array(
			'@type' => ( 'person' === $represents ) ? 'Person' : 'Organization',
			'@id'   => $org_id,
			'name'  => (string) ( class_exists( 'AIEO_DMM_Settings' ) ? ( AIEO_DMM_Settings::get( 'seo.org_name', '' ) ?: $site ) : $site ),
			'url'   => $home,
		);
		$logo = class_exists( 'AIEO_DMM_Settings' ) ? (string) AIEO_DMM_Settings::get( 'seo.org_logo', '' ) : '';
		if ( '' === $logo ) {
			$logo_id = (int) get_option( 'site_icon' );
			if ( $logo_id ) {
				$src  = wp_get_attachment_image_src( $logo_id, 'full' );
				$logo = $src ? (string) $src[0] : '';
			}
		}
		if ( '' !== $logo ) {
			$org['logo'] = array( '@type' => 'ImageObject', 'url' => $logo );
		}
		$same_as = class_exists( 'AIEO_DMM_Settings' ) ? AIEO_DMM_Settings::get( 'seo.social_urls', array() ) : array();
		if ( is_array( $same_as ) && ! empty( $same_as ) ) {
			$org['sameAs'] = array_values( array_filter( array_map( 'strval', $same_as ) ) );
		}
		
		if ( class_exists( 'AIEO_DMM_Settings' ) ) {
			$phone = (string) AIEO_DMM_Settings::get( 'seo.org_phone', '' );
			if ( '' !== $phone ) {
				$org['telephone'] = $phone;
			}
			$email = (string) AIEO_DMM_Settings::get( 'seo.org_email', '' );
			if ( '' !== $email ) {
				$org['email'] = $email;
			}
			$addr = array_filter( array(
				'streetAddress'   => (string) AIEO_DMM_Settings::get( 'seo.org_street', '' ),
				'addressLocality' => (string) AIEO_DMM_Settings::get( 'seo.org_locality', '' ),
				'addressRegion'   => (string) AIEO_DMM_Settings::get( 'seo.org_region', '' ),
				'postalCode'      => (string) AIEO_DMM_Settings::get( 'seo.org_postcode', '' ),
				'addressCountry'  => (string) AIEO_DMM_Settings::get( 'seo.org_country', '' ),
			), static function ( $v ) { return '' !== $v; } );
			if ( ! empty( $addr ) ) {
				$org['address'] = array_merge( array( '@type' => 'PostalAddress' ), $addr );
			}
		}
		
		
		
		
		
		$local_nodes = array();
		if ( class_exists( 'AIEO_DMM_Settings' ) && AIEO_DMM_Settings::get( 'seo.locations_schema_enabled' )
			&& class_exists( 'AIEO_SEO_Local_Business_Builder' ) ) {
			$lb = AIEO_SEO_Local_Business_Builder::build_nodes( $org_id );
			if ( ! empty( $lb['nodes'] ) ) {
				$local_nodes            = $lb['nodes'];
				$org['subOrganization'] = $lb['refs'];
				
				if ( is_string( $org['@type'] ) && 'Organization' === $org['@type'] ) {
					$org['@type'] = array( 'Organization', 'OnlineStore' );
				}
				
				if ( empty( $org['address'] ) && ! empty( $lb['hq'] ) ) {
					$hq_addr = AIEO_SEO_Local_Business_Builder::address_for( $lb['hq'] );
					if ( ! empty( $hq_addr ) ) {
						$org['address'] = $hq_addr;
					}
					if ( empty( $org['telephone'] ) && ! empty( $lb['hq']['phone'] ) ) {
						$org['telephone'] = (string) $lb['hq']['phone'];
					}
				}
			}
		}
		$graph[] = $org;
		foreach ( $local_nodes as $ln ) {
			$graph[] = $ln;
		}

		
		$graph[] = array(
			'@type'           => 'WebSite',
			'@id'             => $site_id,
			'url'             => $home,
			'name'            => $site,
			'publisher'       => array( '@id' => $org_id ),
			'potentialAction' => array(
				'@type'       => 'SearchAction',
				'target'      => array(
					'@type'       => 'EntryPoint',
					'urlTemplate' => $home . '?s={search_term_string}',
				),
				'query-input' => 'required name=search_term_string',
			),
		);

		
		if ( 'product' === $ctx['object_type'] && function_exists( 'wc_get_product' ) ) {
			$node = self::product_node( $ctx, $title, $desc, $canon, $org_id );
			if ( $node ) {
				$graph[] = $node;
			}
		} elseif ( 'post' === $ctx['kind'] ) {
			$graph[] = self::webpage_node( $ctx, $title, $desc, $canon, $site_id, $org_id );
		} elseif ( 'term' === $ctx['kind'] ) {
			$collection = array(
				'@type'       => 'CollectionPage',
				'@id'         => $canon . '#collection',
				'url'         => $canon,
				'name'        => $title,
				'description' => $desc,
				'isPartOf'    => array( '@id' => $site_id ),
				'inLanguage'  => self::lang_code(),
				'breadcrumb'  => array( '@id' => $canon . '#breadcrumb' ),
			);
			
			
			if ( 'product_brand' === $ctx['object_type'] ) {
				$collection['about'] = array( '@type' => 'Brand', 'name' => $ctx['name'] );
			}
			
			
			
			$items = self::term_item_list( $ctx['object_type'], (int) $ctx['object_id'] );
			if ( ! empty( $items ) ) {
				$collection['mainEntity'] = $items;
			}
			$graph[] = $collection;
		}

		
		
		
		
		
		
		$faq = self::faq_node( $ctx, $canon, $site_id );
		if ( $faq ) {
			$graph[] = $faq;
		}

		
		$crumbs = self::breadcrumb_node( $ctx, $home, $canon );
		if ( $crumbs ) {
			$graph[] = $crumbs;
		}

		return array(
			'@context' => 'https://schema.org',
			'@graph'   => $graph,
		);
	}

	private static function product_node( $ctx, $title, $desc, $canon, $org_id ) {
		$pid  = (int) $ctx['object_id'];
		$vrow = self::vitals_row( $pid );
		$vars = self::vitals_variations( $pid );

		
		
		
		if ( ! $vrow && empty( $vars ) ) {
			return self::product_node_wc( $ctx, $desc, $canon );
		}

		$is_variable = ! empty( $vars );
		$node        = array(
			'@type'       => $is_variable ? 'ProductGroup' : 'Product',
			'@id'         => $canon . '#product',
			'name'        => $ctx['name'],
			'url'         => $canon,
			'description' => $desc,
		);
		$sku = $vrow ? (string) ( $vrow['sku'] ?? '' ) : '';
		if ( '' !== $sku ) {
			$node['sku'] = $sku;
			if ( $is_variable ) {
				
				
				
				$node['productGroupID'] = ( strlen( $sku ) <= 50 ) ? $sku : (string) $pid;
			}
		}
		if ( $vrow && ! empty( $vrow['gtin'] ) ) {
			$node['gtin'] = (string) $vrow['gtin'];
		}
		$img = self::image_for( array(), $ctx );
		if ( '' === $img && $vrow && ! empty( $vrow['image_url'] ) ) {
			$img = (string) $vrow['image_url'];
		}
		if ( '' !== $img ) {
			$node['image'] = $img;
		}
		if ( $vrow && ! empty( $vrow['brand'] ) ) {
			$node['brand'] = array( '@type' => 'Brand', 'name' => (string) $vrow['brand'] );
		}

		
		
		
		
		$node['color'] = (string) ( is_array( $vrow ) ? ( $vrow['color'] ?? '' ) : '' );

		if ( $is_variable ) {
			
			
			
			$has_color = false;
			$has_size  = false;
			foreach ( $vars as $v ) {
				if ( ! empty( $v['color'] ) ) { $has_color = true; }
				if ( ! empty( $v['size'] ) )  { $has_size = true; }
			}
			$varies = array();
			if ( $has_color ) { $varies[] = 'https://schema.org/color'; }
			if ( $has_size )  { $varies[] = 'https://schema.org/size'; }
			if ( ! empty( $varies ) ) {
				$node['variesBy'] = $varies;
			}
			$variants = array();
			foreach ( $vars as $v ) {
				$vid   = (int) $v['product_id'];
				$vnode = array(
					'@type' => 'Product',
					'@id'   => $canon . '#variant-' . $vid,
				);
				$vname = (string) ( ! empty( $v['name'] ) ? $v['name'] : ( $v['product_name'] ?? '' ) );
				if ( '' !== $vname ) { $vnode['name'] = $vname; }
				if ( ! empty( $v['sku'] ) )       { $vnode['sku']   = (string) $v['sku']; }
				if ( ! empty( $v['gtin'] ) )      { $vnode['gtin']  = (string) $v['gtin']; }
				if ( ! empty( $v['image_url'] ) ) { $vnode['image'] = (string) $v['image_url']; }
				if ( ! empty( $v['color'] ) )     { $vnode['color'] = (string) $v['color']; }
				if ( ! empty( $v['size'] ) )      { $vnode['size']  = (string) $v['size']; }
				
				
				
				
				$v_own_desc = '';
				if ( ! empty( $v['short_description'] ) ) {
					$v_own_desc = (string) $v['short_description'];
				} elseif ( ! empty( $v['product_description'] ) ) {
					$v_own_desc = (string) $v['product_description'];
				}
				$v_own_desc = trim( wp_strip_all_tags( $v_own_desc ) );
				$v_desc     = trim( (string) $desc );
				if ( '' !== $v_own_desc && false === stripos( $v_desc, $v_own_desc ) ) {
					$v_desc = ( '' !== $v_desc ) ? ( $v_desc . ' ' . $v_own_desc ) : $v_own_desc;
				}
				if ( '' !== $v_desc ) {
					$vnode['description'] = self::truncate( $v_desc, 500 );
				}
				$voffer = self::build_offer( $v, $canon );
				if ( ! empty( $voffer ) ) { $vnode['offers'] = $voffer; }
				$variants[] = $vnode;
			}
			if ( ! empty( $variants ) ) {
				$node['hasVariant'] = $variants;
			}
		} elseif ( $vrow ) {
			$offer = self::build_offer( $vrow, $canon );
			if ( ! empty( $offer ) ) {
				$node['offers'] = $offer;
			}
		}

		
		if ( class_exists( 'AIEO_Ratings_Schema' ) ) {
			$agg = AIEO_Ratings_Schema::build_aggregate_rating( $pid );
			if ( is_array( $agg ) && ! empty( $agg ) ) {
				$node['aggregateRating'] = $agg;
			}
			if ( method_exists( 'AIEO_Ratings_Schema', 'build_reviews' ) ) {
				$revs = AIEO_Ratings_Schema::build_reviews( $pid, $agg );
				if ( ! empty( $revs ) ) { $node['review'] = $revs; }
			}
		}
		return $node;
	}

	
	private static function build_offer( $row, $url ) {
		$price = isset( $row['price'] ) ? (string) $row['price'] : '';
		if ( '' === $price || (float) $price <= 0 ) {
			return array();
		}
		$currency = get_woocommerce_currency();
		$offer    = array(
			'@type'         => 'Offer',
			'url'           => $url,
			'price'         => wc_format_decimal( $price, wc_get_price_decimals() ),
			'priceCurrency' => $currency,
			'availability'  => self::avail( isset( $row['stock_status'] ) ? (string) $row['stock_status'] : '' ),
		);
		
		
		
		$regular = isset( $row['regular_price'] ) ? (string) $row['regular_price'] : '';
		if ( '' !== $regular && (float) $regular > (float) $price ) {
			$offer['priceSpecification'] = array(
				'@type'         => 'UnitPriceSpecification',
				'priceType'     => 'https://schema.org/ListPrice',
				'price'         => wc_format_decimal( $regular, wc_get_price_decimals() ),
				'priceCurrency' => $currency,
			);
		}
		$ret = self::merchant_return_policy();
		if ( ! empty( $ret ) ) {
			$offer['hasMerchantReturnPolicy'] = $ret;
		}
		$ship = self::offer_shipping_details();
		if ( ! empty( $ship ) ) {
			$offer['shippingDetails'] = $ship;
		}
		return $offer;
	}

	
	private static function avail( $status ) {
		$s = strtolower( trim( (string) $status ) );
		if ( 'outofstock' === $s ) {
			return 'https://schema.org/OutOfStock';
		}
		if ( 'onbackorder' === $s ) {
			return 'https://schema.org/BackOrder';
		}
		return 'https://schema.org/InStock';
	}

	
	
	private static function vitals_row( $product_id ) {
		global $wpdb;
		$t   = $wpdb->prefix . 'aieo_core_product_vitals';
		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT product_id, name, product_name, sku, gtin, price, regular_price, onsale, stock_status, color, size, image_url, brand
				 FROM `{$t}` WHERE product_id = %d LIMIT 1",
				(int) $product_id
			),
			ARRAY_A
		);
		return is_array( $row ) ? $row : null;
	}

	
	private static function vitals_variations( $parent_id ) {
		global $wpdb;
		$t    = $wpdb->prefix . 'aieo_core_product_vitals';
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT product_id, name, product_name, sku, gtin, price, regular_price, onsale, stock_status, color, size, image_url, short_description, product_description
				 FROM `{$t}` WHERE parent_product_id = %d AND product_id <> parent_product_id ORDER BY product_id ASC LIMIT 50",
				(int) $parent_id
			),
			ARRAY_A
		);
		return is_array( $rows ) ? $rows : array();
	}
	

	
	private static function product_node_wc( $ctx, $desc, $canon ) {
		$product = wc_get_product( $ctx['object_id'] );
		if ( ! $product ) {
			return null;
		}
		$node = array(
			'@type'       => 'Product',
			'@id'         => $canon . '#product',
			'name'        => $ctx['name'],
			'url'         => $canon,
			'description' => $desc,
		);
		$sku = $product->get_sku();
		if ( $sku ) {
			$node['sku'] = $sku;
		}
		$img = self::image_for( array(), $ctx );
		if ( '' !== $img ) {
			$node['image'] = $img;
		}
		$brands = get_the_terms( $ctx['object_id'], 'product_brand' );
		if ( $brands && ! is_wp_error( $brands ) ) {
			$node['brand'] = array( '@type' => 'Brand', 'name' => $brands[0]->name );
		}
		
		
		$node['color'] = '';
		$price = $product->get_price();
		if ( '' !== (string) $price ) {
			$node['offers'] = self::build_offer(
				array(
					'price'         => $price,
					'regular_price' => $product->get_regular_price(),
					'onsale'        => $product->is_on_sale() ? 1 : 0,
					'stock_status'  => $product->get_stock_status(),
				),
				$canon
			);
		} elseif ( $product->is_type( 'variable' ) ) {
			$node['offers'] = array(
				'@type'         => 'AggregateOffer',
				'priceCurrency' => get_woocommerce_currency(),
				'lowPrice'      => wc_format_decimal( $product->get_variation_price( 'min' ), wc_get_price_decimals() ),
				'highPrice'     => wc_format_decimal( $product->get_variation_price( 'max' ), wc_get_price_decimals() ),
				'offerCount'    => count( $product->get_children() ),
				'availability'  => $product->is_in_stock() ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock',
				'url'           => $canon,
			);
		}
		if ( class_exists( 'AIEO_Ratings_Schema' ) ) {
			$agg = AIEO_Ratings_Schema::build_aggregate_rating( $ctx['object_id'] );
			if ( is_array( $agg ) && ! empty( $agg ) ) {
				$node['aggregateRating'] = $agg;
			}
			if ( method_exists( 'AIEO_Ratings_Schema', 'build_reviews' ) ) {
				$revs = AIEO_Ratings_Schema::build_reviews( $ctx['object_id'], $agg );
				if ( ! empty( $revs ) ) { $node['review'] = $revs; }
			}
		}
		return $node;
	}

	
	private static function term_item_list( $taxonomy, $term_id ) {
		global $wpdb;
		$t = $wpdb->prefix . 'aieo_core_product_vitals';
		if ( 'product_cat' === $taxonomy ) {
			$cond = 'FIND_IN_SET( %d, category_ids )';
		} elseif ( 'product_brand' === $taxonomy ) {
			$cond = 'brand_id = %d';
		} elseif ( 'product_tag' === $taxonomy ) {
			$cond = 'FIND_IN_SET( %d, tag_ids )';
		} else {
			return array();
		}
		
		
		
		
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT product_id, name, product_name, product_url, link_to_view
				 FROM `{$t}`
				 WHERE product_id = parent_product_id AND {$cond}
				 ORDER BY product_latest_sale_date DESC, product_id DESC LIMIT 30",
				(int) $term_id
			),
			ARRAY_A
		);
		if ( empty( $rows ) ) {
			return array();
		}
		$elements = array();
		$pos      = 1;
		foreach ( $rows as $r ) {
			
			
			$url = '';
			foreach ( array( (string) ( $r['product_url'] ?? '' ), (string) ( $r['link_to_view'] ?? '' ) ) as $cand ) {
				if ( '' !== $cand && ! self::is_ugly_term_url( $cand ) ) {
					$url = $cand;
					break;
				}
			}
			if ( '' === $url ) {
				$url = (string) get_permalink( (int) $r['product_id'] );
			}
			if ( '' === $url ) {
				continue;
			}
			$elements[] = array(
				'@type'    => 'ListItem',
				'position' => $pos++,
				'url'      => $url,
				'name'     => (string) ( ! empty( $r['name'] ) ? $r['name'] : ( $r['product_name'] ?? '' ) ),
			);
		}
		if ( empty( $elements ) ) {
			return array();
		}
		return array(
			'@type'           => 'ItemList',
			'numberOfItems'   => count( $elements ),
			'itemListElement' => $elements,
		);
	}

	
	private static function lang_code() {
		$l = (string) get_bloginfo( 'language' );
		if ( '' === $l ) {
			$l = (string) get_locale();
		}
		return strtolower( substr( str_replace( '_', '-', $l ), 0, 2 ) );
	}

	
	private static function merchant_return_policy() {
		$s = self::seo_settings();
		if ( empty( $s['return_enabled'] ) ) {
			return array();
		}
		$days = (int) ( $s['return_days'] ?? 0 );
		if ( $days <= 0 ) {
			return array();
		}
		$country = strtoupper( (string) ( $s['return_country'] ?? '' ) );
		return array(
			'@type'                => 'MerchantReturnPolicy',
			'applicableCountry'    => '' !== $country ? $country : 'GR',
			'returnPolicyCategory' => 'https://schema.org/MerchantReturnFiniteReturnWindow',
			'merchantReturnDays'   => $days,
			'returnMethod'         => 'https://schema.org/ReturnByMail',
			'returnFees'           => ! empty( $s['return_free'] ) ? 'https://schema.org/FreeReturn' : 'https://schema.org/ReturnShippingFees',
		);
	}

	
	private static function offer_shipping_details() {
		$s = self::seo_settings();
		if ( empty( $s['ship_enabled'] ) ) {
			return array();
		}
		$country = strtoupper( (string) ( $s['ship_country'] ?? '' ) );
		$rate    = ( isset( $s['ship_rate'] ) && '' !== $s['ship_rate'] ) ? (float) $s['ship_rate'] : 0.0;
		$details = array(
			'@type'               => 'OfferShippingDetails',
			'shippingDestination' => array( '@type' => 'DefinedRegion', 'addressCountry' => '' !== $country ? $country : 'GR' ),
			'shippingRate'        => array(
				'@type'    => 'MonetaryAmount',
				'value'    => wc_format_decimal( $rate, wc_get_price_decimals() ),
				'currency' => get_woocommerce_currency(),
			),
		);
		$h_max = (int) ( $s['handling_max'] ?? 0 );
		$t_max = (int) ( $s['transit_max'] ?? 0 );
		if ( $h_max > 0 || $t_max > 0 ) {
			$h_min = (int) ( $s['handling_min'] ?? 0 );
			$t_min = (int) ( $s['transit_min'] ?? 0 );
			$details['deliveryTime'] = array(
				'@type'        => 'ShippingDeliveryTime',
				'handlingTime' => array( '@type' => 'QuantitativeValue', 'minValue' => $h_min, 'maxValue' => max( $h_min, $h_max ), 'unitCode' => 'DAY' ),
				'transitTime'  => array( '@type' => 'QuantitativeValue', 'minValue' => $t_min, 'maxValue' => max( $t_min, $t_max ), 'unitCode' => 'DAY' ),
			);
		}
		return $details;
	}

	
	private static function faq_node( $ctx, $canon, $site_id ) {
		if ( 'product' !== ( $ctx['object_type'] ?? '' ) ) {
			return null; 
		}
		$s = self::seo_settings();
		if ( empty( $s['schema_faq_enabled'] ) ) {
			return null; 
		}

		$name  = isset( $ctx['name'] ) ? (string) $ctx['name'] : '';
		$pairs = array();

		
		
		$vrow = self::vitals_row( (int) $ctx['object_id'] );
		if ( $vrow && '' !== (string) ( $vrow['stock_status'] ?? '' ) ) {
			$in_stock = ( 'outofstock' !== strtolower( trim( (string) $vrow['stock_status'] ) ) );
			$pairs[]  = array(
				self::faq_q( 'availability', $name ),
				$in_stock
					? __( 'Yes, it is in stock and available to order now.', 'ai-eshop-optimizer' )
					: __( 'It is currently out of stock.', 'ai-eshop-optimizer' ),
			);
		}

		
		if ( ! empty( $s['return_enabled'] ) && (int) ( $s['return_days'] ?? 0 ) > 0 ) {
			$days = (int) $s['return_days'];
			$ans  = sprintf(
				
				_n( 'You can return this product within %d day of delivery.', 'You can return this product within %d days of delivery.', $days, 'ai-eshop-optimizer' ),
				$days
			);
			$ans    .= ' ' . ( ! empty( $s['return_free'] )
				? __( 'Return shipping is free.', 'ai-eshop-optimizer' )
				: __( 'Return shipping is paid by the customer.', 'ai-eshop-optimizer' ) );
			$pairs[] = array( self::faq_q( 'returns', $name ), $ans );
		}

		
		if ( ! empty( $s['ship_enabled'] ) ) {
			$rate = ( isset( $s['ship_rate'] ) && '' !== $s['ship_rate'] ) ? (float) $s['ship_rate'] : 0.0;
			$ans  = ( $rate > 0 )
				? sprintf(
					
					__( 'Standard shipping costs %s.', 'ai-eshop-optimizer' ),
					trim( html_entity_decode( wp_strip_all_tags( wc_price( $rate ) ), ENT_QUOTES, 'UTF-8' ) )
				)
				: __( 'Shipping is free.', 'ai-eshop-optimizer' );
			$t_max = (int) ( $s['transit_max'] ?? 0 );
			if ( $t_max > 0 ) {
				$t_min = (int) ( $s['transit_min'] ?? 0 );
				$ans  .= ' ' . sprintf(
					
					__( 'Estimated delivery time is %1$d–%2$d business days.', 'ai-eshop-optimizer' ),
					max( 0, $t_min ),
					max( $t_min, $t_max )
				);
			}
			$pairs[] = array( self::faq_q( 'shipping', $name ), $ans );
		}

		if ( empty( $pairs ) ) {
			return null;
		}

		$main = array();
		foreach ( $pairs as $p ) {
			$main[] = array(
				'@type'          => 'Question',
				'name'           => $p[0],
				'acceptedAnswer' => array( '@type' => 'Answer', 'text' => $p[1] ),
			);
		}
		return array(
			'@type'      => 'FAQPage',
			'@id'        => $canon . '#faq',
			'url'        => $canon,
			'inLanguage' => self::lang_code(),
			'isPartOf'   => array( '@id' => $site_id ),
			'mainEntity' => $main,
		);
	}

	
	private static function faq_q( $kind, $name ) {
		$n = ( '' !== (string) $name ) ? (string) $name : __( 'this product', 'ai-eshop-optimizer' );
		switch ( $kind ) {
			case 'availability':
				
				return sprintf( __( 'Is %s in stock?', 'ai-eshop-optimizer' ), $n );
			case 'returns':
				
				return sprintf( __( 'Can I return %s?', 'ai-eshop-optimizer' ), $n );
			case 'shipping':
			default:
				
				return sprintf( __( 'How is %s shipped and how long does delivery take?', 'ai-eshop-optimizer' ), $n );
		}
	}

	
	private static function seo_settings() {
		static $cache = null;
		if ( null !== $cache ) {
			return $cache;
		}
		if ( class_exists( 'AIEO_DMM_Settings' ) ) {
			$s     = AIEO_DMM_Settings::get( 'seo', array() );
			$cache = is_array( $s ) ? $s : array();
			return $cache;
		}
		$opt   = get_option( 'aieo_dmm_settings' );
		$cache = ( is_array( $opt ) && isset( $opt['seo'] ) && is_array( $opt['seo'] ) ) ? $opt['seo'] : array();
		return $cache;
	}

	private static function webpage_node( $ctx, $title, $desc, $canon, $site_id, $org_id ) {
		$post    = $ctx['post'];
		$is_post = ( 'page' !== $ctx['object_type'] );
		$node    = array(
			'@type'    => $is_post ? 'Article' : 'WebPage',
			'@id'      => $canon . '#webpage',
			'url'      => $canon,
			'name'     => $title,
			'isPartOf' => array( '@id' => $site_id ),
		);
		if ( '' !== (string) $desc ) {
			$node['description'] = $desc;
		}
		if ( $is_post && $post ) {
			$node['headline']      = $ctx['name'];
			$node['datePublished'] = get_post_time( 'c', true, $post );
			$node['dateModified']  = get_post_modified_time( 'c', true, $post );
			$node['author']        = array( '@type' => 'Person', 'name' => get_the_author_meta( 'display_name', (int) $post->post_author ) );
			$node['publisher']     = array( '@id' => $org_id );
			$img                   = self::image_for( array(), $ctx );
			if ( '' !== $img ) {
				$node['image'] = $img;
			}
		}
		return $node;
	}

	private static function breadcrumb_node( $ctx, $home, $canon ) {
		
		
		
		
		
		$trail = class_exists( 'AIEO_SEO_Breadcrumb' )
			? AIEO_SEO_Breadcrumb::items( $ctx['object_type'], (int) $ctx['object_id'] )
			: array(
				array( 'name' => get_bloginfo( 'name' ), 'url' => $home ),
				array( 'name' => $ctx['name'], 'url' => $canon ),
			);

		$elements = array();
		$pos      = 1;
		foreach ( $trail as $crumb ) {
			$el = array(
				'@type'    => 'ListItem',
				'position' => $pos++,
				'name'     => isset( $crumb['name'] ) ? (string) $crumb['name'] : '',
			);
			if ( ! empty( $crumb['url'] ) ) {
				$el['item'] = (string) $crumb['url'];
			}
			$elements[] = $el;
		}

		return array(
			'@type'           => 'BreadcrumbList',
			'@id'             => $canon . '#breadcrumb',
			'itemListElement' => $elements,
		);
	}

	

	private static function assemble_head_html( $canon, $og, $twitter, $graph ) {
		$out = array();
		if ( '' !== (string) $canon ) {
			$out[] = '<link rel="canonical" href="' . esc_url( $canon ) . '" />';
		}
		foreach ( $og as $prop => $val ) {
			$out[] = '<meta property="' . esc_attr( $prop ) . '" content="' . esc_attr( $val ) . '" />';
		}
		foreach ( $twitter as $name => $val ) {
			$out[] = '<meta name="' . esc_attr( $name ) . '" content="' . esc_attr( $val ) . '" />';
		}
		
		
		
		
		
		$json = wp_json_encode( $graph, JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
		if ( false !== $json ) {
			$out[] = '<script type="application/ld+json">' . $json . '</script>';
		}
		return implode( "\n", $out );
	}

	
	private static function expand_tokens( $format, $ctx ) {
		if ( '' === (string) $format ) {
			return '';
		}
		$sep  = (string) ( class_exists( 'AIEO_DMM_Settings' ) ? AIEO_DMM_Settings::get( 'seo.separator', '-' ) : '-' );
		$map  = array(
			'#site_title'         => get_bloginfo( 'name' ),
			'#tagline'            => get_bloginfo( 'description' ),
			'#separator_sa'       => $sep,
			'#post_title'         => $ctx['name'],
			'#taxonomy_title'     => $ctx['name'],
			'#taxonomy_description' => ( 'term' === $ctx['kind'] && $ctx['term'] ) ? wp_strip_all_tags( term_description( $ctx['term']->term_id, $ctx['term']->taxonomy ) ) : '',
			'#archive_title'      => $ctx['name'],
			'#post_excerpt'       => ( 'post' === $ctx['kind'] && $ctx['post'] ) ? wp_strip_all_tags( get_the_excerpt( $ctx['post'] ) ) : '',
			'#post_content'       => ( 'post' === $ctx['kind'] && $ctx['post'] ) ? wp_strip_all_tags( wp_trim_words( $ctx['post']->post_content, 55, '' ) ) : '',
			'#current_year'       => gmdate( 'Y' ),
			'#search_term'        => '',
		);
		return trim( preg_replace( '/\s+/u', ' ', strtr( $format, $map ) ) );
	}

	
	
	

	private static function write_row( $object_type, $object_id, $lang, $title, $canon, $robots, $head_html, $hash ) {
		global $wpdb;
		$t   = AIEO_SEO_Head::table();
		$now = current_time( 'mysql', true );

		$existing = $wpdb->get_row(
			$wpdb->prepare(
				
				"SELECT id, content_hash FROM `{$t}` WHERE object_type = %s AND object_id = %d AND lang = %s",
				$object_type,
				$object_id,
				$lang
			),
			ARRAY_A
		);
		$id = $existing ? (int) $existing['id'] : 0;

		
		
		if ( $id > 0 && isset( $existing['content_hash'] ) && (string) $existing['content_hash'] === (string) $hash ) {
			return;
		}

		$data    = array(
			'title'        => $title,
			'canonical'    => $canon,
			'robots'       => $robots,
			'head_html'    => $head_html,
			'content_hash' => $hash,
			'updated'      => $now,
		);
		$formats = array( '%s', '%s', '%s', '%s', '%s', '%s' );

		if ( $id > 0 ) {
			$wpdb->update( $t, $data, array( 'id' => $id ), $formats, array( '%d' ) );
		} else {
			$data['object_type'] = $object_type;
			$data['object_id']   = $object_id;
			$data['lang']        = $lang;
			$formats[]           = '%s';
			$formats[]           = '%d';
			$formats[]           = '%s';
			$wpdb->insert( $t, $data, $formats );
		}

		if ( class_exists( 'AIEO_SEO_Head' ) ) {
			wp_cache_delete( AIEO_SEO_Head::cache_key( $object_type, $object_id, $lang ), AIEO_SEO_Head::CACHE_GROUP );
		}
	}

	private static function delete_row( $object_type, $object_id, $lang ) {
		global $wpdb;
		$t = AIEO_SEO_Head::table();
		
		$exists = (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) === $t;
		if ( ! $exists ) {
			return;
		}
		$wpdb->delete( $t, array( 'object_type' => $object_type, 'object_id' => $object_id, 'lang' => $lang ), array( '%s', '%d', '%s' ) );
		if ( class_exists( 'AIEO_SEO_Head' ) ) {
			wp_cache_delete( AIEO_SEO_Head::cache_key( $object_type, $object_id, $lang ), AIEO_SEO_Head::CACHE_GROUP );
		}
	}

	
	private static function delete_all( $object_type, $object_id ) {
		global $wpdb;
		$t = AIEO_SEO_Head::table();
		
		$exists = (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) === $t;
		if ( ! $exists ) {
			return;
		}
		$wpdb->delete( $t, array( 'object_type' => $object_type, 'object_id' => $object_id ), array( '%s', '%d' ) );
		if ( class_exists( 'AIEO_SEO_Head' ) ) {
			wp_cache_delete( AIEO_SEO_Head::cache_key( $object_type, $object_id, '' ), AIEO_SEO_Head::CACHE_GROUP );
			$lang = AIEO_SEO_Head::current_lang();
			if ( '' !== $lang ) {
				wp_cache_delete( AIEO_SEO_Head::cache_key( $object_type, $object_id, $lang ), AIEO_SEO_Head::CACHE_GROUP );
			}
		}
	}

	
	
	
	
	
	
	
	
	
	

	const CURSOR_OPTION = 'aieo_seo_head_backfill_cursor';

	
	const DIRTY_OPTION = 'aieo_seo_head_dirty';
	const DIRTY_CAP    = 5000;

	
	private static function sync_colors_to_vitals() {
		if ( ! class_exists( 'AIEO_DMM_Settings' ) ) {
			return;
		}
		$seo   = (array) AIEO_DMM_Settings::get( 'seo', array() );
		$taxes = ( isset( $seo['color_attribute_taxonomies'] ) && is_array( $seo['color_attribute_taxonomies'] ) )
			? $seo['color_attribute_taxonomies'] : array();
		$taxes = array_values( array_filter( array_map( 'sanitize_key', (array) $taxes ) ) );
		if ( empty( $taxes ) ) {
			return;
		}

		global $wpdb;
		$vitals = $wpdb->prefix . 'aieo_core_product_vitals';
		foreach ( $taxes as $tax ) {
			
			$wpdb->query(
				$wpdb->prepare(
					"UPDATE `{$vitals}` v
					 JOIN (
						SELECT tr.object_id AS pid, MIN(t.name) AS color_name
						FROM {$wpdb->term_relationships} tr
						JOIN {$wpdb->term_taxonomy} tt
							ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = %s
						JOIN {$wpdb->terms} t ON t.term_id = tt.term_id
						GROUP BY tr.object_id
					 ) c ON c.pid = v.product_id
					 SET v.color = c.color_name
					 WHERE ( v.color IS NULL OR v.color = '' )",
					$tax
				)
			);
		}
	}

	
	public static function run_backfill( $limit = 2000, $cursor_in = null ) {
		$out = array( 'built' => 0, 'phase' => 'posts', 'done' => true, 'cursor' => array( 'phase' => 0, 'last_id' => 0, 'last_tax' => '' ) );
		if ( ! class_exists( 'AIEO_SEO_Store' ) || ! AIEO_SEO_Store::is_active_provider() ) {
			return $out;
		}

		$budget     = max( 1, (int) $limit );
		$standalone = is_array( $cursor_in );           
		if ( $standalone ) {
			$cursor = self::normalise_cursor( $cursor_in );
		} elseif ( true === $cursor_in ) {
			$cursor = array( 'phase' => 0, 'last_id' => 0, 'last_tax' => '' );
		} else {
			$cursor = self::get_cursor();
		}
		$wrapped = false;

		
		
		
		
		if ( 0 === (int) $cursor['phase'] && 0 === (int) $cursor['last_id'] ) {
			if ( class_exists( 'AIEO_Ratings' ) && method_exists( 'AIEO_Ratings', 'sync_all_ratings_to_vitals' ) ) {
				AIEO_Ratings::sync_all_ratings_to_vitals();
			}
			self::sync_colors_to_vitals();
		}

		
		if ( 0 === (int) $cursor['phase'] && $budget > 0 ) {
			$rows = self::next_post_batch( (int) $cursor['last_id'], $budget );
			foreach ( $rows as $row ) {
				
				
				
				$cursor['last_id'] = $row['id'];
				try {
					self::build( $row['type'], $row['id'] );
					$out['built']++;
				} catch ( \Throwable $e ) {
					self::log_build_error( $row['type'], $row['id'], $e );
				}
				$budget--;
			}
			if ( empty( $rows ) ) {
				$cursor = array( 'phase' => 1, 'last_id' => 0, 'last_tax' => '' ); 
			}
		}

		
		
		if ( 1 === (int) $cursor['phase'] && $budget > 0 ) {
			$out['phase'] = 'terms';
			$rows         = self::next_term_batch( (int) $cursor['last_id'], (string) $cursor['last_tax'], $budget );
			foreach ( $rows as $row ) {
				$cursor['last_id']  = $row['id'];
				$cursor['last_tax'] = $row['tax'];
				try {
					self::build( $row['tax'], $row['id'] );
					$out['built']++;
				} catch ( \Throwable $e ) {
					self::log_build_error( $row['tax'], $row['id'], $e );
				}
				$budget--;
			}
			if ( empty( $rows ) ) {
				$cursor  = array( 'phase' => 0, 'last_id' => 0, 'last_tax' => '' ); 
				$wrapped = true;
			}
		}

		if ( ! $standalone ) {
			self::save_cursor( $cursor );
		}
		$out['done']   = $wrapped;
		$out['cursor'] = $cursor;
		return $out;
	}

	private static function log_build_error( $type, $id, $e ) {
		if ( function_exists( 'aieo_debug_log' ) ) {
			aieo_debug_log( 'AIEO_SEO head build failed for ' . $type . ':' . (int) $id . ' — ' . $e->getMessage() );
		}
	}

	

	
	public static function mark_dirty( $object_type, $object_id ) {
		$object_type = (string) $object_type;
		$object_id   = (int) $object_id;
		if ( '' === $object_type || $object_id <= 0 ) {
			return;
		}
		$list = get_option( self::DIRTY_OPTION );
		if ( ! is_array( $list ) ) {
			$list = array();
		}
		$key = $object_type . ':' . $object_id;
		if ( isset( $list[ $key ] ) || count( $list ) >= self::DIRTY_CAP ) {
			return; 
		}
		$list[ $key ] = 1;
		update_option( self::DIRTY_OPTION, $list, false );
	}

	
	public static function drain_dirty( $limit ) {
		$list = get_option( self::DIRTY_OPTION );
		if ( ! is_array( $list ) || empty( $list ) ) {
			return 0;
		}
		$built = 0;
		$keys  = array_slice( array_keys( $list ), 0, max( 1, (int) $limit ) );
		foreach ( $keys as $key ) {
			$parts = explode( ':', $key, 2 );
			if ( 2 === count( $parts ) ) {
				try {
					self::build( $parts[0], (int) $parts[1] );
					$built++;
				} catch ( \Throwable $e ) {
					self::log_build_error( $parts[0], (int) $parts[1], $e );
				}
			}
			unset( $list[ $key ] );
		}
		if ( empty( $list ) ) {
			delete_option( self::DIRTY_OPTION );
		} else {
			update_option( self::DIRTY_OPTION, $list, false );
		}
		return $built;
	}

	private static function normalise_cursor( $c ) {
		return array(
			'phase'    => ( is_array( $c ) && isset( $c['phase'] ) ) ? (int) $c['phase'] : 0,
			'last_id'  => ( is_array( $c ) && isset( $c['last_id'] ) ) ? (int) $c['last_id'] : 0,
			'last_tax' => ( is_array( $c ) && isset( $c['last_tax'] ) ) ? (string) $c['last_tax'] : '',
		);
	}

	private static function get_cursor() {
		return self::normalise_cursor( get_option( self::CURSOR_OPTION ) );
	}

	private static function save_cursor( $cursor ) {
		update_option( self::CURSOR_OPTION, self::normalise_cursor( $cursor ), false );
	}

	
	private static function viewable_post_types() {
		$out = array();
		foreach ( get_post_types( array( 'public' => true ), 'names' ) as $t ) {
			if ( in_array( $t, array( 'attachment', 'revision', 'nav_menu_item' ), true ) ) {
				continue;
			}
			$out[] = $t;
		}
		return $out;
	}

	
	private static function is_public_content_type( $type ) {
		if ( in_array( (string) $type, array( 'attachment', 'revision', 'nav_menu_item' ), true ) ) {
			return false;
		}
		$pto = get_post_type_object( (string) $type );
		return $pto && ! empty( $pto->public );
	}

	private static function viewable_taxonomies() {
		$out = array();
		foreach ( get_taxonomies( array( 'public' => true ), 'names' ) as $tx ) {
			if ( is_taxonomy_viewable( $tx ) ) {
				$out[] = $tx;
			}
		}
		return $out;
	}

	
	private static function next_post_batch( $after_id, $limit ) {
		$types = self::viewable_post_types();
		if ( empty( $types ) ) {
			return array();
		}
		global $wpdb;
		$in   = "'" . implode( "','", array_map( 'esc_sql', $types ) ) . "'";
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				
				"SELECT ID, post_type FROM {$wpdb->posts}
				 WHERE post_status = 'publish' AND post_type IN ($in) AND ID > %d
				 ORDER BY ID ASC LIMIT %d",
				(int) $after_id,
				(int) $limit
			)
		);
		$out = array();
		foreach ( (array) $rows as $r ) {
			$out[] = array( 'type' => (string) $r->post_type, 'id' => (int) $r->ID );
		}
		return $out;
	}

	
	private static function next_term_batch( $after_id, $after_tax, $limit ) {
		$taxes = self::viewable_taxonomies();
		if ( empty( $taxes ) ) {
			return array();
		}
		global $wpdb;
		$in   = "'" . implode( "','", array_map( 'esc_sql', $taxes ) ) . "'";
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				
				"SELECT t.term_id, tt.taxonomy FROM {$wpdb->terms} t
				 INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_id = t.term_id
				 WHERE tt.taxonomy IN ($in)
				   AND ( t.term_id > %d OR ( t.term_id = %d AND tt.taxonomy > %s ) )
				 ORDER BY t.term_id ASC, tt.taxonomy ASC LIMIT %d",
				(int) $after_id,
				(int) $after_id,
				(string) $after_tax,
				(int) $limit
			)
		);
		$out = array();
		foreach ( (array) $rows as $r ) {
			$out[] = array( 'tax' => (string) $r->taxonomy, 'id' => (int) $r->term_id );
		}
		return $out;
	}
}
