<?php
/**
 * Class to handle the SEO functionality for the GoDAM block.
 *
 * @package GoDAM
 */

namespace RTGODAM\Inc;

defined( 'ABSPATH' ) || exit;

use RTGODAM\Inc\Traits\Singleton;

/**
 * Class Seo
 */
class Seo {

	use Singleton;

	const VIDEO_SEO_SCHEMA_META_KEY         = 'godam_video_seo_schema';
	const VIDEO_SEO_SCHEMA_UPDATED_META_KEY = 'godam_video_seo_schema_updated';
	const ATTACHMENT_POSTS_MAP_META_KEY     = 'godam_posts_using_attachment';
	const POST_ATTACHMENTS_META_KEY         = '_godam_seo_attachments';

	/**
	 * Construct method.
	 */
	protected function __construct() {
		$this->setup_hooks();
	}

	/**
	 * Setup hooks.
	 *
	 * @return void
	 */
	public function setup_hooks() {
		add_action( 'save_post', array( $this, 'save_seo_data_as_postmeta' ), 10, 2 );
		add_action( 'save_post', array( $this, 'elementor_save_seo_data_as_postmeta' ), 10, 1 );
		add_filter( 'rest_prepare_attachment', array( $this, 'add_video_duration_for_video_seo' ), 10, 2 );
		add_action( 'wp_head', array( $this, 'add_video_seo_schema' ) );

		// Hook to update SEO when attachment is edited.
		add_action( 'edit_attachment', array( $this, 'schedule_seo_sync_for_attachment' ) );
		add_action( 'godam_sync_attachment_seo', array( $this, 'sync_seo_for_attachment_posts' ) );
	}

	/**
	 * Save SEO schema data from 'godam/video' blocks as post meta.
	 *
	 * This function parses the Gutenberg block content of the post and extracts
	 * any `seo` attribute from blocks of type `godam/video`. The extracted data
	 * is then saved in the post meta under the key `godam_video_seo_schema`,
	 * along with a timestamp in `godam_video_seo_schema_updated`.
	 *
	 * Add-ons can contribute additional schemas via the
	 * `godam_video_seo_extra_block_schemas` filter.
	 *
	 * Also handles WPBakery shortcodes in the same post.
	 *
	 * @param int     $post_ID Post ID.
	 * @param WP_Post $post    Post object.
	 */
	public function save_seo_data_as_postmeta( $post_ID, $post ) {
		// Bail if this is an autosave or revision.
		if ( wp_is_post_autosave( $post_ID ) || wp_is_post_revision( $post_ID ) ) {
			return;
		}

		// Ensure we're working with a valid WP_Post object.
		if ( ! $post instanceof \WP_Post ) {
			return;
		}

		// Skip if this is an Elementor post (handled separately).
		$is_elementor = $this->is_elementor_post( $post_ID );
		if ( $is_elementor ) {
			return;
		}

		$content = $post->post_content;

		$video_seo_schema = array();
		$attachments_used = array();

		// Parse Gutenberg blocks if content contains godam/video blocks.
		$has_video_block = ! empty( $content ) && strpos( $content, '<!-- wp:godam/video' ) !== false;

		if ( $has_video_block ) {
			$blocks = parse_blocks( $content );

			foreach ( $blocks as $block ) {
				$result           = $this->extract_video_seo_schema_from_block( $block, true );
				$video_seo_schema = array_merge( $video_seo_schema, $result['schemas'] );
				$attachments_used = array_merge( $attachments_used, $result['attachments'] );
			}
		}

		// Parse WPBakery shortcodes if they exist.
		if ( ! empty( $content ) && has_shortcode( $content, 'godam_video' ) ) {
			$result           = $this->godam_get_video_seo_data_from_wpbakery( $content, true );
			$video_seo_schema = array_merge( $video_seo_schema, $result['schemas'] );
			$attachments_used = array_merge( $attachments_used, $result['attachments'] );
		}

		/**
		 * Filter to let add-ons contribute extra block-based video SEO schemas.
		 *
		 * For example, the GoDAM for WooCommerce add-on uses this to parse
		 * `godam/video-product-gallery` (Shoppable Video) blocks and return
		 * the resulting VideoObject SEO entries.
		 *
		 * @since 1.10.0
		 *
		 * @param array  $extra_schemas Array of extra SEO data arrays (initially empty).
		 * @param string $content       The post content.
		 * @param int    $post_ID       The post ID.
		 */
		$extra_schemas = apply_filters( 'godam_video_seo_extra_block_schemas', array(), $content, $post_ID );
		if ( is_array( $extra_schemas ) ) {
			$video_seo_schema = array_merge( $video_seo_schema, $extra_schemas );
		}

		/**
		 * Filter to let add-ons contribute extra attachment IDs for the
		 * attachment ↔ post mapping used by `edit_attachment` SEO resync.
		 *
		 * For example, the GoDAM for WooCommerce add-on uses this to register
		 * video attachments from `godam/video-product-gallery` blocks, which
		 * are not parsed by the core `extract_video_seo_schema_from_block()`.
		 *
		 * @since 1.10.0
		 *
		 * @param int[]  $extra_attachments Array of extra attachment IDs (initially empty).
		 * @param string $content           The post content.
		 * @param int    $post_ID           The post ID.
		 */
		$extra_attachments = apply_filters( 'godam_video_seo_extra_block_attachments', array(), $content, $post_ID );
		if ( is_array( $extra_attachments ) ) {
			$attachments_used = array_merge( $attachments_used, $extra_attachments );
		}

		if ( ! empty( $video_seo_schema ) ) {
			/**
			 * Filter the video SEO schema data before it is cached as post meta.
			 *
			 * Allows add-ons (e.g. WooCommerce integration) to enrich each video's
			 * cached SEO data with additional information such as product details.
			 *
			 * @since 1.10.0
			 *
			 * @param array $video_seo_schema Array of video SEO data arrays.
			 * @param int   $post_ID          The post ID being saved.
			 */
			$video_seo_schema = apply_filters( 'godam_video_seo_cache_data', $video_seo_schema, $post_ID );

			update_post_meta( $post_ID, self::VIDEO_SEO_SCHEMA_META_KEY, $video_seo_schema );
			update_post_meta( $post_ID, self::VIDEO_SEO_SCHEMA_UPDATED_META_KEY, time() );
			$this->update_attachment_post_mapping( $post_ID, array_unique( $attachments_used ) );

			/**
			 * Fired after video SEO schema is saved as post meta.
			 *
			 * Add-ons can use this to perform additional save-time operations,
			 * such as storing reverse-lookup meta for gallery block product IDs.
			 *
			 * @since 1.10.0
			 *
			 * @param int   $post_ID          The post ID.
			 * @param array $video_seo_schema The saved schema array.
			 */
			do_action( 'godam_video_seo_schema_saved', $post_ID, $video_seo_schema );
		} else {
			delete_post_meta( $post_ID, self::VIDEO_SEO_SCHEMA_META_KEY );
			delete_post_meta( $post_ID, self::VIDEO_SEO_SCHEMA_UPDATED_META_KEY );
			$this->update_attachment_post_mapping( $post_ID, array() );

			/**
			 * Fired after video SEO schema is cleared from a post.
			 *
			 * @since 1.10.0
			 *
			 * @param int $post_ID The post ID.
			 */
			do_action( 'godam_video_seo_schema_cleared', $post_ID );
		}
	}

	/**
	 * Extract SEO schema from a block.
	 *
	 * If the block has seoOverride set to false or not set, it will fetch SEO from the media library.
	 * Otherwise, it will use the SEO data from the block attributes.
	 *
	 * @param array $block             Block data.
	 * @param bool  $track_attachments Whether to track attachments used.
	 * @return array Contains 'schemas' and 'attachments' arrays when tracking, otherwise just schemas.
	 */
	private function extract_video_seo_schema_from_block( $block, $track_attachments = false ) {
		$schemas     = array();
		$attachments = array();

		if ( isset( $block['blockName'] ) && 'godam/video' === $block['blockName'] ) {
			$seo_override  = isset( $block['attrs']['seoOverride'] ) ? $block['attrs']['seoOverride'] : false;
			$attachment_id = isset( $block['attrs']['id'] ) ? (int) $block['attrs']['id'] : 0;

			if ( $seo_override && isset( $block['attrs']['seo'] ) && ! empty( $block['attrs']['seo'] ) ) {
				// Use overridden SEO from block attributes.
				$seo_entry = $block['attrs']['seo'];

				// Include attachment_id so add-ons can resolve the attachment
				// on the first save (before _godam_seo_attachments exists).
				if ( $attachment_id > 0 ) {
					$seo_entry['attachment_id'] = $attachment_id;
				}

				$schemas[] = $seo_entry;
			} elseif ( $attachment_id > 0 ) {
				// Fetch SEO from media library attachment.
				$media_seo = $this->get_seo_from_attachment( $attachment_id );
				if ( ! empty( $media_seo ) ) {
					$media_seo['attachment_id'] = $attachment_id;
					$schemas[]                  = $media_seo;
					if ( $track_attachments ) {
						$attachments[] = $attachment_id;
					}
				}
			} elseif ( isset( $block['attrs']['seo'] ) && ! empty( $block['attrs']['seo'] ) ) {
				// Fallback to block SEO if no attachment ID.
				$schemas[] = $block['attrs']['seo'];
			}
		}

		if ( ! empty( $block['innerBlocks'] ) ) {
			foreach ( $block['innerBlocks'] as $inner_block ) {
				$result = $this->extract_video_seo_schema_from_block( $inner_block, $track_attachments );
				if ( $track_attachments ) {
					$schemas     = array_merge( $schemas, $result['schemas'] );
					$attachments = array_merge( $attachments, $result['attachments'] );
				} else {
					$schemas = array_merge( $schemas, $result );
				}
			}
		}

		if ( $track_attachments ) {
			return array(
				'schemas'     => $schemas,
				'attachments' => $attachments,
			);
		}

		return $schemas;
	}

	/**
	 * Get SEO data from a media library attachment.
	 *
	 * @since 1.7.0
	 *
	 * @param int $attachment_id The attachment ID.
	 * @return array The SEO data from the attachment.
	 */
	public function get_seo_from_attachment( $attachment_id ) {
		$attachment = get_post( $attachment_id );

		if ( ! $attachment || 'attachment' !== $attachment->post_type ) {
			return array();
		}

		$meta = get_post_meta( $attachment_id );

		// Get transcoded URL or fallback to attachment URL.
		$content_url = '';
		if ( ! empty( $meta['rtgodam_transcoded_url'][0] ) ) {
			$content_url = $meta['rtgodam_transcoded_url'][0];
		} else {
			$content_url = wp_get_attachment_url( $attachment_id );
		}

		// Get video duration in ISO 8601 format.
		$duration        = '';
		$attachment_meta = wp_get_attachment_metadata( $attachment_id );
		if ( ! empty( $attachment_meta['length'] ) && is_numeric( $attachment_meta['length'] ) ) {
			$duration = $this->seconds_to_iso8601( (int) $attachment_meta['length'] );
		}

		// Get thumbnail URL.
		$thumbnail_url = '';
		if ( ! empty( $meta['rtgodam_media_video_thumbnail'][0] ) ) {
			$thumbnail_url = $meta['rtgodam_media_video_thumbnail'][0];
		}

		// Get upload date in ISO 8601 format.
		$upload_date = '';
		if ( ! empty( $attachment->post_date_gmt ) && '0000-00-00 00:00:00' !== $attachment->post_date_gmt ) {
			$upload_date = gmdate( 'c', strtotime( $attachment->post_date_gmt ) );
		}

		// Strip HTML from description.
		$description = wp_strip_all_tags( $attachment->post_content );

		return array(
			'contentUrl'       => $content_url,
			'headline'         => $attachment->post_title,
			'description'      => $description,
			'uploadDate'       => $upload_date,
			'duration'         => $duration,
			'thumbnailUrl'     => $thumbnail_url,
			'isFamilyFriendly' => true,
		);
	}

	/**
	 * Convert seconds to ISO 8601 duration format.
	 *
	 * @param int $seconds Duration in seconds.
	 * @return string ISO 8601 duration string.
	 */
	private function seconds_to_iso8601( $seconds ) {
		$hours   = floor( $seconds / 3600 );
		$minutes = floor( ( $seconds % 3600 ) / 60 );
		$secs    = $seconds % 60;

		$iso_duration = 'PT';
		if ( $hours > 0 ) {
			$iso_duration .= $hours . 'H';
		}
		if ( $minutes > 0 ) {
			$iso_duration .= $minutes . 'M';
		}
		if ( $secs > 0 || 'PT' === $iso_duration ) {
			$iso_duration .= $secs . 'S';
		}

		return $iso_duration;
	}

	/**
	 * Check if a post is built with Elementor.
	 *
	 * @param int $post_id The post ID.
	 * @return bool True if it's an Elementor post, false otherwise.
	 */
	public function is_elementor_post( $post_id ) {
		return 'builder' === get_post_meta( $post_id, '_elementor_edit_mode', true );
	}

	/**
	 * Add ISO 8601 video duration to REST API response for video attachments.
	 *
	 * @param WP_REST_Response $response  The response object.
	 * @param WP_Post          $post      The attachment post object.
	 * @return WP_REST_Response
	 */
	public function add_video_duration_for_video_seo( $response, $post ) {
		if ( 'video' === $post->post_mime_type || str_starts_with( $post->post_mime_type, 'video/' ) ) {
			$meta = wp_get_attachment_metadata( $post->ID );

			if ( ! empty( $meta['length'] ) && is_numeric( $meta['length'] ) ) {
				$response->data['video_duration_iso8601'] = $this->seconds_to_iso8601( (int) $meta['length'] );
			}
		}

		return $response;
	}

	/**
	 * Outputs structured data for VideoObject schema on singular pages.
	 *
	 * This function reads cached SEO data from post meta for optimal performance.
	 * The SEO data is cached when the post is saved (in save_seo_data_as_postmeta).
	 *
	 * The schema includes properties like name, description, content URL,
	 * thumbnail, upload date, duration, and family-friendly status.
	 *
	 * Only executes on singular pages and if valid cached SEO data exists.
	 *
	 * @return void
	 */
	public function add_video_seo_schema() {
		if ( ! is_singular() ) {
			return;
		}

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

		// Read cached SEO schema from post meta (fast!).
		$cached_schemas = get_post_meta( $post_id, self::VIDEO_SEO_SCHEMA_META_KEY, true );

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

		$output_schemas    = array();
		$seen_content_urls = array();

		// Build a set of contentUrls claimed by VPG (Shoppable Video) entries.
		// When the same video appears in both a godam/video block and a VPG block,
		// the VPG version takes priority (it carries product data via the add-on).
		$vpg_content_urls = array();
		foreach ( $cached_schemas as $video ) {
			if ( ! empty( $video['_source'] ) && 'vpg' === $video['_source'] && ! empty( $video['contentUrl'] ) && is_string( $video['contentUrl'] ) ) {
				$vpg_content_urls[ $video['contentUrl'] ] = true;
			}
		}

		foreach ( $cached_schemas as $video ) {
			if ( ! is_array( $video ) || empty( $video['headline'] ) ) {
				continue;
			}

			$content_url = ! empty( $video['contentUrl'] ) && is_string( $video['contentUrl'] ) ? $video['contentUrl'] : '';
			$is_vpg      = ! empty( $video['_source'] ) && 'vpg' === $video['_source'];

			// Skip standalone entries whose video already exists in a VPG block.
			if ( ! $is_vpg && $content_url && isset( $vpg_content_urls[ $content_url ] ) ) {
				continue;
			}

			// Deduplicate by contentUrl within the same context.
			if ( $content_url && isset( $seen_content_urls[ $content_url ] ) ) {
				continue;
			}
			if ( $content_url ) {
				$seen_content_urls[ $content_url ] = true;
			}

			$schema = array(
				'@context'         => 'https://schema.org',
				'@type'            => 'VideoObject',
				'name'             => sanitize_text_field( $video['headline'] ?? '' ),
				'description'      => wp_strip_all_tags( $video['description'] ?? '' ),
				'contentUrl'       => esc_url_raw( $video['contentUrl'] ?? '' ),
				'uploadDate'       => sanitize_text_field( $video['uploadDate'] ?? '' ),
				'isFamilyFriendly' => isset( $video['isFamilyFriendly'] ) ? (bool) $video['isFamilyFriendly'] : true,
			);

			if ( ! empty( $video['thumbnailUrl'] ) ) {
				$schema['thumbnailUrl'] = esc_url_raw( $video['thumbnailUrl'] );
			}

			if ( ! empty( $video['duration'] ) ) {
				$schema['duration'] = sanitize_text_field( $video['duration'] );
			}

			/**
			 * Filter an individual video SEO schema entry before output.
			 *
			 * Allows add-ons to modify or extend a single VideoObject schema,
			 * e.g. by adding an associatedProduct for WooCommerce integration.
			 *
			 * @since 1.10.0
			 *
			 * @param array $schema  The VideoObject schema array.
			 * @param array $video   The raw cached video SEO data.
			 * @param int   $post_id The current post ID.
			 */
			$schema = apply_filters( 'godam_video_seo_schema', $schema, $video, $post_id );

			$output_schemas[] = $schema;
		}

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

		/**
		 * Filter the complete array of video SEO schemas before JSON-LD output.
		 *
		 * Allows add-ons to add, remove, reorder, or group schema entries —
		 * for example, the GoDAM for WooCommerce add-on uses this to wrap
		 * Shoppable Video schemas in an ItemList for carousel rich results.
		 *
		 * A third argument `$cached_schemas` is passed so add-ons can inspect
		 * the raw cached data (including `_source` markers) without an extra
		 * `get_post_meta()` call.
		 *
		 * @since 1.10.0
		 *
		 * @param array $output_schemas  Array of schema arrays (VideoObject entries).
		 * @param int   $post_id         The current post ID.
		 * @param array $cached_schemas  Raw cached SEO data from post meta.
		 */
		$output_schemas = apply_filters( 'godam_video_seo_schemas', $output_schemas, $post_id, $cached_schemas );

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

		// Output a single <script> with all schemas.
		echo '<script type="application/ld+json">' . wp_json_encode(
			count( $output_schemas ) === 1 ? $output_schemas[0] : $output_schemas,
			JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
		) . '</script>';
	}

	/**
	 * Extract SEO data from Elementor godam_video widget for a given post.
	 *
	 * @param int  $post_id           The ID of the post.
	 * @param bool $track_attachments Whether to track attachments used.
	 * @return array Contains 'schemas' and 'attachments' arrays when tracking, otherwise just schemas.
	 */
	public function godam_get_video_seo_data_from_elementor( $post_id, $track_attachments = false ) {
		$empty_result = $track_attachments ? array(
			'schemas'     => array(),
			'attachments' => array(),
		) : array();

		// Bail if not built with Elementor.
		if ( ! did_action( 'elementor/loaded' ) ) {
			return $empty_result;
		}

		if ( ! $this->is_elementor_post( $post_id ) ) {
			return $empty_result;
		}

		// Get the raw Elementor data.
		$data = get_post_meta( $post_id, '_elementor_data', true );
		if ( empty( $data ) ) {
			return $empty_result;
		}

		$widgets = json_decode( $data, true );
		if ( ! is_array( $widgets ) ) {
			return $empty_result;
		}

		$seo_data    = array();
		$attachments = array();
		$seo_obj     = $this;

		$extractor = function ( $elements ) use ( &$seo_data, &$attachments, &$extractor, $seo_obj, $track_attachments ) {
			foreach ( $elements as $element ) {
				$_seo_data = array();
				if (
					isset( $element['widgetType'] ) &&
					'godam-video' === $element['widgetType']
				) {
					$settings = array();
					if ( isset( $element['settings'] ) && is_array( $element['settings'] ) ) {
						$settings = $element['settings'];
					}
					$seo_override = isset( $settings['seo_override'] ) && 'yes' === $settings['seo_override'];

					// If seo_override is false, try to fetch SEO from the media attachment.
					if ( ! $seo_override ) {
						// Get attachment ID from video-file setting.
						$attachment_id = 0;
						if ( isset( $settings['video-file']['id'] ) && is_numeric( $settings['video-file']['id'] ) ) {
							$attachment_id = absint( $settings['video-file']['id'] );
						}

						if ( $attachment_id > 0 ) {
							$media_seo = $seo_obj->get_seo_from_attachment( $attachment_id );
							if ( ! empty( $media_seo ) && ! empty( $media_seo['headline'] ) ) {
								$seo_data[] = $media_seo;
								if ( $track_attachments ) {
									$attachments[] = $attachment_id;
								}
								continue;
							}
						}
					}

					// Use widget settings if seo_override is true or no attachment data available.
					if (
						isset( $settings['seo_content_headline'] ) &&
						! empty( $settings['seo_content_headline'] )
					) {
						$_seo_data['contentUrl']       = isset( $settings['seo_content_url'] ) ? $settings['seo_content_url'] : '';
						$_seo_data['headline']         = isset( $settings['seo_content_headline'] ) ? $settings['seo_content_headline'] : '';
						$_seo_data['description']      = isset( $settings['seo_content_description'] ) ? $settings['seo_content_description'] : '';
						$_seo_data['uploadDate']       = isset( $settings['seo_content_upload_date'] ) ? $settings['seo_content_upload_date'] : '';
						$_seo_data['thumbnailUrl']     = isset( $settings['seo_content_video_thumbnail_url'] ) ? $settings['seo_content_video_thumbnail_url'] : '';
						$_seo_data['isFamilyFriendly'] = isset( $settings['seo_content_family_friendly'] ) ? 'yes' === $settings['seo_content_family_friendly'] : true;
						$_seo_data['duration']         = isset( $settings['seo_content_duration'] ) ? $settings['seo_content_duration'] : '';

						$seo_data[] = $_seo_data;
					}
				}

				if ( isset( $element['elements'] ) && is_array( $element['elements'] ) ) {
					$extractor( $element['elements'] ); // Recurse into inner elements.
				}
			}
		};

		$extractor( $widgets );

		if ( $track_attachments ) {
			return array(
				'schemas'     => $seo_data,
				'attachments' => $attachments,
			);
		}

		return $seo_data;
	}

	/**
	 * Stores the SEO data for elementor.
	 *
	 * @param int $post_ID Post ID.
	 * @return void
	 */
	public function elementor_save_seo_data_as_postmeta( $post_ID ) {
		// Check if Elementor is active and this post uses Elementor.
		if ( ! did_action( 'elementor/loaded' ) ) {
			return;
		}

		$edit_mode = get_post_meta( $post_ID, '_elementor_edit_mode', true );
		if ( 'builder' !== $edit_mode ) {
			return;
		}

		$result           = $this->godam_get_video_seo_data_from_elementor( $post_ID, true );
		$video_seo_schema = $result['schemas'];
		$attachments_used = $result['attachments'];

		if ( ! empty( $video_seo_schema ) ) {
			/** This filter is documented in inc/classes/class-seo.php */
			$video_seo_schema = apply_filters( 'godam_video_seo_cache_data', $video_seo_schema, $post_ID );

			update_post_meta( $post_ID, self::VIDEO_SEO_SCHEMA_META_KEY, $video_seo_schema );
			update_post_meta( $post_ID, self::VIDEO_SEO_SCHEMA_UPDATED_META_KEY, time() );
			$this->update_attachment_post_mapping( $post_ID, array_unique( $attachments_used ) );
			do_action( 'godam_video_seo_schema_saved', $post_ID, $video_seo_schema );
		} else {
			delete_post_meta( $post_ID, self::VIDEO_SEO_SCHEMA_META_KEY );
			delete_post_meta( $post_ID, self::VIDEO_SEO_SCHEMA_UPDATED_META_KEY );
			$this->update_attachment_post_mapping( $post_ID, array() );
			do_action( 'godam_video_seo_schema_cleared', $post_ID );
		}
	}

	/**
	 * Extract SEO data from WPBakery godam_video shortcodes.
	 *
	 * @param string $content           The post content containing shortcodes.
	 * @param bool   $track_attachments Whether to track attachments used.
	 * @return array Contains 'schemas' and 'attachments' arrays when tracking, otherwise just schemas.
	 */
	public function godam_get_video_seo_data_from_wpbakery( $content, $track_attachments = false ) {
		$seo_data    = array();
		$attachments = array();

		if ( empty( $content ) ) {
			return $track_attachments ? array(
				'schemas'     => $seo_data,
				'attachments' => $attachments,
			) : $seo_data;
		}

		// Match all godam_video shortcodes.
		$pattern = get_shortcode_regex( array( 'godam_video' ) );

		if ( preg_match_all( '/' . $pattern . '/s', $content, $matches, PREG_SET_ORDER ) ) {
			foreach ( $matches as $match ) {
				$atts = shortcode_parse_atts( $match[3] );

				if ( empty( $atts ) || ! is_array( $atts ) ) {
					continue;
				}

				$seo_override  = isset( $atts['seo_override'] ) && '1' === $atts['seo_override'];
				$attachment_id = isset( $atts['id'] ) ? absint( $atts['id'] ) : 0;
				$media_seo     = array();

				if ( $attachment_id > 0 ) {
					$media_seo = $this->get_seo_from_attachment( $attachment_id );
				}

				// If seo_override is false, try to fetch SEO from the media attachment.
				if ( ! $seo_override ) {
					if ( ! empty( $media_seo ) && ! empty( $media_seo['headline'] ) ) {
						$seo_data[] = $media_seo;
						if ( $track_attachments ) {
							$attachments[] = $attachment_id;
						}
						continue;
					}
				}

				// Use shortcode attributes if seo_override is true or no attachment data available.
				if ( ! empty( $atts['seo_headline'] ) ) {
					$video_seo = array(
						'contentUrl'       => ! empty( $media_seo['contentUrl'] ) ? $media_seo['contentUrl'] : ( isset( $atts['seo_content_url'] ) ? $atts['seo_content_url'] : '' ),
						'headline'         => isset( $atts['seo_headline'] ) ? $atts['seo_headline'] : '',
						'description'      => isset( $atts['seo_description'] ) ? $atts['seo_description'] : '',
						'uploadDate'       => ! empty( $media_seo['uploadDate'] ) ? $media_seo['uploadDate'] : ( isset( $atts['seo_upload_date'] ) ? $atts['seo_upload_date'] : '' ),
						'thumbnailUrl'     => ! empty( $media_seo['thumbnailUrl'] ) ? $media_seo['thumbnailUrl'] : ( isset( $atts['seo_thumbnail_url'] ) ? $atts['seo_thumbnail_url'] : '' ),
						'isFamilyFriendly' => isset( $atts['seo_family_friendly'] ) ? '1' === $atts['seo_family_friendly'] : true,
						'duration'         => ! empty( $media_seo['duration'] ) ? $media_seo['duration'] : ( isset( $atts['seo_duration'] ) ? $atts['seo_duration'] : '' ),
					);

					$seo_data[] = $video_seo;
					if ( $track_attachments && $attachment_id > 0 && ! empty( $media_seo ) ) {
						$attachments[] = $attachment_id;
					}
				}
			}
		}

		if ( $track_attachments ) {
			return array(
				'schemas'     => $seo_data,
				'attachments' => $attachments,
			);
		}

		return $seo_data;
	}

	/**
	 * Update the mapping of which posts use a specific attachment for SEO schema generation.
	 *
	 * @param int   $post_id     The post ID.
	 * @param array $attachments Array of attachment IDs used in the post.
	 */
	private function update_attachment_post_mapping( $post_id, $attachments ) {
		// Normalize: cast to positive integers and discard zeroes/non-numeric values.
		$attachments = array_values( array_unique( array_filter( array_map( 'absint', $attachments ) ) ) );

		// Get current attachments this post was using.
		$previous_attachments = get_post_meta( $post_id, self::POST_ATTACHMENTS_META_KEY, true );
		$previous_attachments = is_array( $previous_attachments ) ? $previous_attachments : array();

		// Remove post from old attachments' mapping.
		$removed_attachments = array_diff( $previous_attachments, $attachments );
		foreach ( $removed_attachments as $attachment_id ) {
			$posts_using = get_post_meta( $attachment_id, self::ATTACHMENT_POSTS_MAP_META_KEY, true );
			$posts_using = is_array( $posts_using ) ? $posts_using : array();
			$posts_using = array_diff( $posts_using, array( $post_id ) );
			if ( ! empty( $posts_using ) ) {
				update_post_meta( $attachment_id, self::ATTACHMENT_POSTS_MAP_META_KEY, array_values( $posts_using ) );
			} else {
				delete_post_meta( $attachment_id, self::ATTACHMENT_POSTS_MAP_META_KEY );
			}
		}

		// Add post to new attachments' mapping.
		$new_attachments = array_diff( $attachments, $previous_attachments );
		foreach ( $new_attachments as $attachment_id ) {
			$posts_using = get_post_meta( $attachment_id, self::ATTACHMENT_POSTS_MAP_META_KEY, true );
			$posts_using = is_array( $posts_using ) ? $posts_using : array();
			if ( ! in_array( $post_id, $posts_using, true ) ) {
				$posts_using[] = $post_id;
				update_post_meta( $attachment_id, self::ATTACHMENT_POSTS_MAP_META_KEY, $posts_using );
			}
		}

		// Update post's attachment list.
		if ( ! empty( $attachments ) ) {
			update_post_meta( $post_id, self::POST_ATTACHMENTS_META_KEY, $attachments );
		} else {
			delete_post_meta( $post_id, self::POST_ATTACHMENTS_META_KEY );
		}
	}

	/**
	 * Schedule a background job to sync SEO for all posts using an attachment.
	 *
	 * @param int $attachment_id The attachment ID.
	 */
	public function schedule_seo_sync_for_attachment( $attachment_id ) {
		$attachment = get_post( $attachment_id );

		// Only process video attachments.
		if ( ! $attachment || strpos( $attachment->post_mime_type, 'video/' ) !== 0 ) {
			return;
		}

		// Check if any posts are using this attachment.
		$posts_using = get_post_meta( $attachment_id, self::ATTACHMENT_POSTS_MAP_META_KEY, true );
		if ( empty( $posts_using ) || ! is_array( $posts_using ) ) {
			return;
		}

		// Schedule the sync to run in the background.
		if ( ! wp_next_scheduled( 'godam_sync_attachment_seo', array( $attachment_id ) ) ) {
			wp_schedule_single_event( time(), 'godam_sync_attachment_seo', array( $attachment_id ) );
		}
	}

	/**
	 * Sync SEO for all posts using a specific attachment.
	 * This runs as a background task when an attachment is updated.
	 *
	 * @param int $attachment_id The attachment ID.
	 */
	public function sync_seo_for_attachment_posts( $attachment_id ) {
		$posts_using = get_post_meta( $attachment_id, self::ATTACHMENT_POSTS_MAP_META_KEY, true );

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

		foreach ( $posts_using as $post_id ) {
			$post = get_post( $post_id );
			if ( ! $post ) {
				continue;
			}

			// Check if it's an Elementor post.
			$is_elementor = $this->is_elementor_post( $post_id );

			if ( $is_elementor ) {
				$this->elementor_save_seo_data_as_postmeta( $post_id );
			} else {
				$this->save_seo_data_as_postmeta( $post_id, $post );
			}
		}
	}
}
