<?php
/**
 * The Video Parser
 *
 * @since      2.0.0
 * @package    RankMath
 * @subpackage RankMath\Schema\Video
 * @author     Rank Math <support@rankmath.com>
 */

namespace RankMathPro\Schema\Video;

use RankMath\Helper;
use RankMath\Helpers\Str;
use RankMath\Schema\DB;

defined( 'ABSPATH' ) || exit;

/**
 * Parser class.
 */
class Parser {

	/**
	 * Post.
	 *
	 * @var WP_Post
	 */
	private $post;

	/**
	 * Stored Video URLs.
	 *
	 * @var array
	 */
	private $urls;

	/**
	 * Stores video ids for re-use within this class methods.
	 *
	 * @var array
	 */
	private $shortcode_ids = [];

	/**
	 * The Constructor.
	 *
	 * @param  WP_Post $post Post to parse.
	 */
	public function __construct( $post ) {
		$this->post = $post;
	}

	/**
	 * Save video object.
	 *
	 * @param boolean $save Whether to create new Video schema.
	 * This parameter is used to short-circuit the process from the v3.0.60 update routine file created to delete the Video schema.
	 */
	public function save( $save = true ) {
		if (
			! ( $this->post instanceof \WP_Post ) ||
			wp_is_post_revision( $this->post->ID ) ||
			! Helper::get_settings( "titles.pt_{$this->post->post_type}_autodetect_video", 'on' )
		) {
			return;
		}

		$content = trim( $this->post->post_content . ' ' . $this->get_custom_fields_data() );
		if ( empty( $content ) ) {
			return;
		}

		$content = apply_filters( 'the_content', $content );

		/**
		 * Filter to change the content passed to the Video Parser.
		 *
		 * @param string $content Post Content.
		 * @param object $post    Post Object.
		 *
		 * @return string Content.
		 */
		$content    = apply_filters( 'rank_math/video/parser_content', $content, $this->post );
		$this->urls = $this->get_video_urls( $content );

		if ( ! $save ) {
			return;
		}

		$allowed_types = apply_filters( 'media_embedded_in_content_allowed_types', [ 'video', 'embed', 'iframe' ] );
		$tags          = implode( '|', $allowed_types );
		$videos        = [];

		preg_match_all( '#<(?P<tag>' . $tags . ')[^<]*?(?:>[\s\S]*?<\/(?P=tag)>|\s*\/>)#', $content, $matches );
		if ( ! empty( $matches ) && ! empty( $matches[0] ) ) {
			foreach ( $matches[0] as $html ) {
				$videos[] = $this->get_metadata( $html );
			}
		}

		$videos = array_merge( $videos, $this->get_metadata_from_shortcode( $content ) );
		$videos = array_filter(
			$videos,
			function ( $video ) {
				return ! empty( $video['src'] ) ? $video['src'] : false;
			}
		);

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

		$schemas = $this->get_default_schema_data();
		foreach ( $videos as $video ) {
			$schemas[] = [
				'@type'            => 'VideoObject',
				'metadata'         => [
					'title'                   => 'Video',
					'type'                    => 'template',
					'shortcode'               => uniqid( 's-' ),
					'isPrimary'               => empty( DB::get_schemas( $this->post->ID ) ),
					'reviewLocationShortcode' => '[rank_math_rich_snippet]',
					'category'                => '%categories%',
					'tags'                    => '%tags%',
					'isAutoGenerated'         => true,
				],
				'name'             => ! empty( $video['name'] ) ? $video['name'] : '%seo_title%',
				'description'      => ! empty( $video['description'] ) ? $video['description'] : '%seo_description%',
				'uploadDate'       => ! empty( $video['uploadDate'] ) ? $video['uploadDate'] : '%date(Y-m-dTH:i:sP)%',
				'thumbnailUrl'     => ! empty( $video['thumbnail'] ) ? $video['thumbnail'] : '%post_thumbnail%',
				'embedUrl'         => ! empty( $video['embed'] ) ? $video['src'] : '',
				'contentUrl'       => empty( $video['embed'] ) ? $video['src'] : '',
				'duration'         => ! empty( $video['duration'] ) ? $video['duration'] : '',
				'width'            => ! empty( $video['width'] ) ? $video['width'] : '',
				'height'           => ! empty( $video['height'] ) ? $video['height'] : '',
				'isFamilyFriendly' => ! empty( $video['isFamilyFriendly'] ) ? (bool) filter_var( $video['isFamilyFriendly'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ) : true,
			];
		}

		foreach ( array_filter( $schemas ) as $schema ) {
			add_post_meta( $this->post->ID, "rank_math_schema_{$schema['@type']}", $schema );
		}
	}

	/**
	 * Get default schema data.
	 */
	private function get_default_schema_data() {
		if ( ! empty( DB::get_schemas( $this->post->ID ) ) ) {
			return [];
		}

		$default_type = Helper::get_default_schema_type( $this->post->ID, true );
		if ( ! $default_type ) {
			return [];
		}

		$is_article  = in_array( $default_type, [ 'Article', 'NewsArticle', 'BlogPosting' ], true );
		$schema_data = [];
		if ( $is_article ) {
			$schema_data = [
				'headline'      => Helper::get_settings( "titles.pt_{$this->post->post_type}_default_snippet_name" ),
				'description'   => Helper::get_settings( "titles.pt_{$this->post->post_type}_default_snippet_desc" ),
				'datePublished' => '%date(Y-m-dTH:i:sP)%',
				'dateModified'  => '%modified(Y-m-dTH:i:sP)%',
				'keywords'      => '%keywords%',
				'image'         => [
					'@type' => 'ImageObject',
					'url'   => '%post_thumbnail%',
				],
				'author'        => [
					'@type' => 'Person',
					'name'  => '%name%',
				],
			];
		}

		$schema_data['@type']    = $default_type;
		$schema_data['metadata'] = [
			'title'     => Helper::sanitize_schema_title( $default_type ),
			'type'      => 'template',
			'isPrimary' => true,
		];

		return [ $schema_data ];
	}

	/**
	 * Get Video source from the content.
	 *
	 * @param array $html Video Links.
	 *
	 * @return array
	 */
	public function get_metadata( $html ) {
		preg_match_all( '@src=[\'"]([^"]+)[\'"]@', $html, $matches );
		if ( empty( $matches ) || empty( $matches[1] ) ) {
			return false;
		}

		return $this->get_video_metadata( $matches[1][0] );
	}

	/**
	 * Validate Video source.
	 *
	 * @param string  $url            Video Source.
	 * @param boolean $generate_image Whether to generate the image.
	 *
	 * @return array
	 */
	private function get_video_metadata( $url, $generate_image = true ) {
		$url = ! Str::contains( 'vimeo.com', $url ) && ! Str::contains( 'watch?v=', $url ) ? preg_replace( '/\?.*/', '', $url ) : $url; // Remove query string from URL.
		if (
			$url &&
			(
				is_array( $this->urls ) &&
				(
					in_array( $url, $this->urls, true ) ||
					in_array( $url . '?feature=oembed', $this->urls, true )
				)
			)
		) {
			return false;
		}

		$this->urls[] = $url;
		$networks     = [
			'Video\Youtube',
			'Video\Vimeo',
			'Video\DailyMotion',
			'Video\TedVideos',
			'Video\VideoPress',
			'Video\WordPress',
		];

		$data = false;
		foreach ( $networks as $network ) {
			$args = 'Video\WordPress' !== $network ? $url : [
				'url'  => $url,
				'post' => $this->post,
			];
			$data = \call_user_func( [ '\\RankMathPro\\Schema\\' . $network, 'match' ], $args );
			if ( is_array( $data ) ) {
				break;
			}
		}

		// Save image locally.
		if ( ! empty( $data['thumbnail'] ) && $generate_image ) {
			$data['thumbnail'] = $this->save_video_thumbnail( $data );
		}

		return $data;
	}


	/**
	 * Get Video URLs from YouTube Embed plugin.
	 *
	 * @param string $content Post Content.
	 *
	 * @return array
	 */
	private function get_metadata_from_shortcode( $content ) {
		$data = [];
		foreach ( $this->get_ids_from_shortcode( $content ) as $video_id ) {
			$data[] = $this->get_video_metadata( "https://www.youtube.com/embed/{$video_id}" );
		}

		return $data;
	}

	/**
	 * Get Video IDs from YouTube Embed plugin.
	 *
	 * @param  string $content Post Content.
	 * @return array
	 *
	 * Credit ridgerunner (https://stackoverflow.com/users/433790/ridgerunner)
	 */
	private function get_ids_from_shortcode( $content ) {
		if ( ! empty( $this->shortcode_ids ) ) {
			return $this->shortcode_ids;
		}
		preg_match_all(
			'~
			https?://          # Required scheme. Either http or https.
			(?:[0-9A-Z-]+\.)?  # Optional subdomain.
			(?:                # Group host alternatives.
			youtu\.be/         # Either youtu.be,
			| youtube          # or youtube.com or
			(?:-nocookie)?     # youtube-nocookie.com
			\.com              # followed by
			\S*?               # Allow anything up to VIDEO_ID,
			[^\w\s-]           # but char before ID is non-ID char.
			)                  # End host alternatives.
			([\w-]{11})        # $1: VIDEO_ID is exactly 11 chars.
			(?=[^\w-]|$)       # Assert next char is non-ID or EOS.
			(?!                # Assert URL is not pre-linked.
			[?=&+%\w.-]*       # Allow URL (query) remainder.
			(?:                # Group pre-linked alternatives.
				[\'"][^<>]*>   # Either inside a start tag,
			| </a>             # or inside <a> element text contents.
			)                  # End recognized pre-linked alts.
			)                  # End negative lookahead assertion.
			[?=&+%\w.-]*       # Consume any URL (query) remainder.
			~ix',
			$content,
			$matches
		);

		$this->shortcode_ids = empty( $matches ) || empty( $matches[1] ) ? [] : $matches[1];

		return $this->shortcode_ids;
	}

	/**
	 * Validate Video source.
	 *
	 * @param  array $data Video data.
	 * @return array
	 *
	 * Credits to m1r0 @ https://gist.github.com/m1r0/f22d5237ee93bcccb0d9
	 */
	private function save_video_thumbnail( $data ) {
		$url = $data['thumbnail'];
		if ( ! Helper::get_settings( "titles.pt_{$this->post->post_type}_autogenerate_image", 'off' ) ) {
			return false;
		}

		if ( Str::starts_with( wp_get_upload_dir()['baseurl'], $url ) ) {
			return $url;
		}

		if ( ! class_exists( 'WP_Http' ) ) {
			include_once ABSPATH . WPINC . '/class-http.php';
		}

		$url      = explode( '?', $url )[0];
		$http     = new \WP_Http();
		$response = $http->request( $url );
		if ( 200 !== $response['response']['code'] ) {
			return false;
		}

		$image_title = __( 'Video Thumbnail', 'rank-math-pro' );
		if ( ! empty( $data['name'] ) ) {
			$image_title = $data['name'];
		} elseif ( ! empty( $this->post->post_title ) ) {
			$image_title = $this->post->post_title;
		}
		$filename = substr( sanitize_title( $image_title, 'video-thumbnail' ), 0, 32 ) . '.jpg';

		/**
		 * Filter the filename of the video thumbnail.
		 *
		 * @param string $filename The filename of the video thumbnail.
		 * @param array  $data     The video data.
		 * @param object $post     The post object.
		 */
		$filename = apply_filters( 'rank_math/schema/video_thumbnail_filename', $filename, $data, $this->post );

		$upload = wp_upload_bits( sanitize_file_name( $filename ), null, $response['body'] );
		if ( ! empty( $upload['error'] ) ) {
			return false;
		}

		$file_path     = $upload['file'];
		$file_name     = basename( $file_path );
		$file_type     = wp_check_filetype( $file_name, null );
		$wp_upload_dir = wp_upload_dir();

		// Translators: Placeholder is the image title.
		$attachment_title = sprintf( __( 'Video Thumbnail: %s', 'rank-math-pro' ), $image_title );

		/**
		 * Filter the attachment title of the video thumbnail.
		 *
		 * @param string $attachment_title The attachment title of the video thumbnail.
		 * @param array  $data             The video data.
		 * @param object $post             The post object.
		 */
		$attachment_title = apply_filters( 'rank_math/schema/video_thumbnail_attachment_title', $attachment_title, $data, $this->post );

		$post_info = [
			'guid'           => $wp_upload_dir['url'] . '/' . $file_name,
			'post_mime_type' => $file_type['type'],
			'post_title'     => $attachment_title,
			'post_content'   => '',
			'post_status'    => 'inherit',
		];

		$attach_id = wp_insert_attachment( $post_info, $file_path, $this->post->ID );

		// Include image.php.
		require_once ABSPATH . 'wp-admin/includes/image.php'; // @phpstan-ignore-line

		// Define attachment metadata.
		$attach_data = wp_generate_attachment_metadata( $attach_id, $file_path );

		// Assign metadata to attachment.
		wp_update_attachment_metadata( $attach_id, $attach_data );

		return wp_get_attachment_url( $attach_id );
	}

	/**
	 * Get Video URls stored in VideoObject schema.
	 *
	 * @param string $content Post content.
	 * @return array
	 */
	private function get_video_urls( $content = '' ) {
		$schemas = DB::get_schemas( $this->post->ID );
		if ( empty( $schemas ) ) {
			return [];
		}

		$urls = [];
		foreach ( $schemas as $key => $schema ) {
			if ( empty( $schema['@type'] ) || 'VideoObject' !== $schema['@type'] ) {
				continue;
			}

			if ( $this->maybe_delete_schema( $key, $schema, $content ) ) {
				continue;
			}

			if ( ! empty( $schema['embedUrl'] ) ) {
				$this->maybe_update_upload_date( $key, $schema );
			}

			$urls[] = ! empty( $schema['embedUrl'] ) ? $schema['embedUrl'] : '';
			$urls[] = ! empty( $schema['contentUrl'] ) ? $schema['contentUrl'] : '';
		}

		return array_filter( $urls );
	}

	/**
	 * Delete video schema if a video is deleted from the content.
	 *
	 * @param string $key     Meta key.
	 * @param array  $schema  Schema data.
	 * @param string $content Post content.
	 *
	 * @return bool
	 */
	private function maybe_delete_schema( $key, $schema, $content ) {
		if ( empty( $schema['metadata']['isAutoGenerated'] ) || ! apply_filters( 'rank_math/video/delete_autogenerated_schema', true ) ) {
			return false;
		}

		$url = ! empty( $schema['embedUrl'] ) ? $schema['embedUrl'] : ( ! empty( $schema['contentUrl'] ) ? $schema['contentUrl'] : '' );

		if ( ! empty( $url ) && ( Str::contains( $url, $content ) || $this->compare_alternate_url( $content, $url ) ) ) {
			return false;
		}

		// If URL not found in post_content, check Elementor's raw data before deleting.
		if ( ! empty( $url ) && $this->is_video_in_elementor_data( $url ) ) {
			return false;
		}

		$meta_id = str_replace( 'schema-', '', $key );
		return delete_metadata_by_mid( 'post', $meta_id );
	}

	/**
	 * Update uploadDate value in the existing Video Schema.
	 *
	 * @param int   $meta_id    Meta id.
	 * @param array $meta_value Schema data.
	 */
	private function maybe_update_upload_date( $meta_id, $meta_value ) {
		if ( empty( $meta_value['uploadDate'] ) ) {
			return;
		}

		$parts = explode( 'T', $meta_value['uploadDate'] );
		if ( ! empty( $parts[1] ) ) {
			return;
		}

		$video_meta = $this->get_video_metadata( $meta_value['embedUrl'], false );
		if ( empty( $video_meta['uploadDate'] ) ) {
			return;
		}

		$meta_id                  = str_replace( 'schema-', '', $meta_id );
		$meta_value['uploadDate'] = $video_meta['uploadDate'];
		update_metadata_by_mid( 'post', $meta_id, $meta_value, 'rank_math_schema_VideoObject' );
	}

	/**
	 * Get Custom fields data.
	 */
	private function get_custom_fields_data() {
		$custom_fields = Str::to_arr_no_empty( Helper::get_settings( 'sitemap.video_sitemap_custom_fields' ) );
		if ( empty( $custom_fields ) ) {
			return;
		}

		$content = '';
		foreach ( $custom_fields as $custom_field ) {
			$content = $content . ' <p>' . get_post_meta( $this->post->ID, $custom_field, true ) . '</p>';
		}

		return trim( $content );
	}

	/**
	 * Checks if the video ID ( in different formats ) is present in the post content.
	 *
	 * @param string $content Post content.
	 * @param string $url     YouTube embed URL already saved with the schema.
	 *
	 * @return bool
	 */
	private function compare_alternate_url( $content, $url ) {
		$id = array_filter(
			$this->get_ids_from_shortcode( $content ),
			function ( $video_id ) use ( $url ) {
				return Str::contains( $video_id, $url );
			}
		);

		if ( empty( $id ) ) {
			return false;
		}
		$pattern = '/https:\/\/(?:(www\.)?youtu\.?be(?:(-nocookie)?\.com)?(\/)?(embed|watch\?v=)?)(\/)?' . current( $id ) . '/';
		return 1 === preg_match( $pattern, $content );
	}

	/**
	 * Check if a video URL is present in Elementor's raw _elementor_data JSON.
	 *
	 * Elementor stores video URLs in widget settings (e.g. youtube_url, vimeo_url)
	 * inside _elementor_data post meta. Videos are rendered client-side so the
	 * server-side HTML never contains an iframe src we can match against.
	 *
	 * @param string $url Embed or content URL from the VideoObject schema.
	 *
	 * @return bool
	 */
	private function is_video_in_elementor_data( $url ) {
		if ( ! get_post_meta( $this->post->ID, '_elementor_edit_mode', true ) ) {
			return false;
		}

		$elementor_data = get_post_meta( $this->post->ID, '_elementor_data', true );
		if ( empty( $elementor_data ) ) {
			return false;
		}

		// Extract the video ID from the schema URL (works for embed and watch URLs).
		$ids = $this->get_ids_from_shortcode( $url );
		if ( ! empty( $ids ) ) {
			foreach ( $ids as $video_id ) {
				if ( Str::contains( $video_id, $elementor_data ) ) {
					return true;
				}
			}
		}

		// Fallback: direct URL substring match (covers Vimeo, self-hosted, etc.).
		return Str::contains( $url, $elementor_data );
	}
}
