<?php
/**
 * Product and Review JSON-LD Schema
 *
 * Outputs aggregateRating and Review schema for UGC reviews. Integrates with
 * Yoast WooCommerce SEO when present to avoid duplicate Product schema.
 *
 * @package Ugcify\SEO
 */

namespace Ugcify\SEO;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class Schema
 */
class Schema {
	/**
	 * Tracks whether Yoast's Product schema filter actually ran this request.
	 *
	 * @var bool
	 */
	private $yoast_product_schema_filter_ran = false;

	/**
	 * Max number of Review objects to include in schema (avoid huge markup).
	 *
	 * @var int
	 */
	const MAX_REVIEWS_IN_SCHEMA = 10;

	/**
	 * Constructor.
	 */
	public function __construct() {
		// When Yoast WooCommerce SEO is active, add our data to their Product schema.
		add_filter( 'wpseo_schema_product', array( $this, 'add_ugcify_to_yoast_product_schema' ), 10, 1 );
		// When Yoast product schema is not used (no Yoast WooCommerce SEO), output our own JSON-LD.
		add_action( 'wp_enqueue_scripts', array( $this, 'maybe_enqueue_standalone_schema' ), 5 );
	}

	/**
	 * Add aggregateRating and review to Yoast's Product schema (no duplicate Product).
	 *
	 * @param array $data Product schema array from Yoast.
	 * @return array Modified schema.
	 */
	public function add_ugcify_to_yoast_product_schema( $data ) {
		$this->yoast_product_schema_filter_ran = true;

		if ( ! is_array( $data ) || empty( $data['@id'] ) ) {
			return $data;
		}

		$data = $this->sanitize_product_schema_image( $data );

		$product_id = $this->get_current_product_id();
		if ( ! $product_id ) {
			return $data;
		}

		$repository = new \Ugcify\Database\ReviewRepository();
		$review_count = $repository->count_rated_reviews( $product_id, 'approved' );
		if ( $review_count < 1 ) {
			return $data;
		}

		$avg_rating = $repository->get_average_rating( $product_id, 'approved' );
		$data['aggregateRating'] = array(
			'@type'       => 'AggregateRating',
			'ratingValue' => (string) number_format( (float) $avg_rating, 1, '.', '' ),
			'reviewCount' => (string) $review_count,
			'bestRating'  => '5',
			'worstRating' => '1',
		);

		$reviews = $repository->get_paginated( array(
			'product_id' => $product_id,
			'status'     => 'approved',
			'rating'     => '1,2,3,4,5',
			'per_page'   => self::MAX_REVIEWS_IN_SCHEMA,
			'page'       => 1,
			'order_by'   => 'id',
			'order'      => 'DESC',
		) );
		$review_schemas = array();
		foreach ( $reviews['items'] as $review ) {
			$author_name = $this->resolve_review_author_name( $review );
			$review_schemas[] = array(
				'@type'        => 'Review',
				'author'       => array(
					'@type' => 'Person',
					'name'  => $author_name,
				),
				'datePublished' => gmdate( 'c', strtotime( $review->created_at ) ),
				'reviewBody'   => wp_strip_all_tags( $review->content ),
				'reviewRating' => array(
					'@type'       => 'Rating',
					'ratingValue' => (string) (int) $review->rating,
					'bestRating'  => '5',
					'worstRating' => '1',
				),
			);
		}
		if ( ! empty( $review_schemas ) ) {
			$data['review'] = $review_schemas;
		}

		return $data;
	}

	/**
	 * Enqueue standalone Product + aggregateRating + Review JSON-LD when Yoast product schema is not used.
	 */
	public function maybe_enqueue_standalone_schema() {
		if ( $this->yoast_product_schema_will_run() ) {
			return;
		}

		$product_id = $this->get_current_product_id();
		if ( ! $product_id || ! function_exists( 'wc_get_product' ) ) {
			return;
		}

		$product = wc_get_product( $product_id );
		if ( ! $product ) {
			return;
		}

		$repository = new \Ugcify\Database\ReviewRepository();
		$review_count = $repository->count_rated_reviews( $product_id, 'approved' );
		if ( $review_count < 1 ) {
			return;
		}

		$avg_rating = $repository->get_average_rating( $product_id, 'approved' );
		$product_url = get_permalink( $product_id );
		$product_name = $product->get_name();
		$image_id = $product->get_image_id();
		$image_url = $image_id ? wp_get_attachment_image_url( $image_id, 'full' ) : '';

		$schema = array(
			'@context' => 'https://schema.org',
			'@type'    => 'Product',
			'@id'      => $product_url . '#product',
			'name'     => $product_name,
			'url'      => $product_url,
			'aggregateRating' => array(
				'@type'       => 'AggregateRating',
				'ratingValue' => (string) number_format( (float) $avg_rating, 1, '.', '' ),
				'reviewCount' => (string) $review_count,
				'bestRating'  => '5',
				'worstRating' => '1',
			),
		);
		if ( $this->is_public_schema_url( $image_url ) ) {
			$schema['image'] = $image_url;
		}

		$reviews = $repository->get_paginated( array(
			'product_id' => $product_id,
			'status'     => 'approved',
			'rating'     => '1,2,3,4,5',
			'per_page'   => self::MAX_REVIEWS_IN_SCHEMA,
			'page'       => 1,
			'order_by'   => 'id',
			'order'      => 'DESC',
		) );
		$review_schemas = array();
		foreach ( $reviews['items'] as $review ) {
			$author_name = $this->resolve_review_author_name( $review );
			$review_schemas[] = array(
				'@type'        => 'Review',
				'author'       => array(
					'@type' => 'Person',
					'name'  => $author_name,
				),
				'datePublished' => gmdate( 'c', strtotime( $review->created_at ) ),
				'reviewBody'   => wp_strip_all_tags( $review->content ),
				'reviewRating' => array(
					'@type'       => 'Rating',
					'ratingValue' => (string) (int) $review->rating,
					'bestRating'  => '5',
					'worstRating' => '1',
				),
			);
		}
		$schema['review'] = $review_schemas;

		// Schedule JSON-LD output in wp_head. JSON-LD schema blocks use
		// type="application/ld+json" and are not JavaScript — wp_add_inline_script
		// is not appropriate here. Hooking into wp_head is the standard WordPress
		// pattern used by Yoast, WooCommerce, and WordPress core itself.
		add_action(
			'wp_head',
			static function () use ( $schema ) {
				echo '<script type="application/ld+json">' . wp_json_encode( $schema ) . "</script>\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_json_encode produces safe JSON.
			},
			99
		);
	}

	/**
	 * Whether Yoast will output Product schema on this request (so we don't duplicate).
	 *
	 * @return bool
	 */
	private function yoast_product_schema_will_run() {
		return $this->yoast_product_schema_filter_ran;
	}

	/**
	 * Get current product ID (single product page).
	 *
	 * @return int 0 if not on a product page.
	 */
	private function get_current_product_id() {
		if ( ! is_singular( 'product' ) ) {
			return 0;
		}
		return (int) get_the_ID();
	}

	/**
	 * Check whether a URL is suitable for schema fields that Google validates as public URLs.
	 *
	 * @param string $url URL to validate.
	 * @return bool
	 */
	private function is_public_schema_url( $url ) {
		if ( empty( $url ) || ! wp_http_validate_url( $url ) ) {
			return false;
		}

		$host = (string) wp_parse_url( $url, PHP_URL_HOST );
		if ( '' === $host ) {
			return false;
		}

		// Exclude typical local/dev hosts that trigger Rich Results URL validation errors.
		if ( in_array( strtolower( $host ), array( 'localhost' ), true ) ) {
			return false;
		}
		if ( preg_match( '/\.(local|test|invalid|example)$/i', $host ) ) {
			return false;
		}

		if ( filter_var( $host, FILTER_VALIDATE_IP ) ) {
			if ( ! filter_var( $host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Remove Product image field from schema when URL is not publicly valid.
	 *
	 * @param array $data Product schema data.
	 * @return array
	 */
	private function sanitize_product_schema_image( array $data ) {
		if ( ! isset( $data['image'] ) ) {
			return $data;
		}

		$image = $data['image'];
		$image_url = '';

		if ( is_string( $image ) ) {
			$image_url = $image;
		} elseif ( is_array( $image ) ) {
			if ( isset( $image['url'] ) && is_string( $image['url'] ) ) {
				$image_url = $image['url'];
			} elseif ( isset( $image[0] ) && is_string( $image[0] ) ) {
				$image_url = $image[0];
			} elseif ( isset( $image[0]['url'] ) && is_string( $image[0]['url'] ) ) {
				$image_url = $image[0]['url'];
			}
		}

		if ( '' !== $image_url && ! $this->is_public_schema_url( $image_url ) ) {
			unset( $data['image'] );
		}

		return $data;
	}

	/**
	 * Resolve review author name with social import fallback.
	 *
	 * @param object $review Review object.
	 * @return string
	 */
	private function resolve_review_author_name( $review ) {
		$author_name = '';

		if ( ! empty( $review->guest_name ) ) {
			$author_name = trim( (string) $review->guest_name );
		} elseif ( ! empty( $review->user_id ) ) {
			$author_name = trim( (string) get_the_author_meta( 'display_name', (int) $review->user_id ) );
		}

		if ( '' !== $author_name ) {
			return $author_name;
		}

		$is_social_type = in_array( $review->type, array( 'instagram', 'youtube', 'tiktok' ), true );
		if ( $is_social_type ) {
			/* translators: %s: social platform name (e.g. Instagram) */
			return sprintf( __( '%s user', 'ugcify' ), ucfirst( (string) $review->type ) );
		}

		return __( 'Anonymous', 'ugcify' );
	}
}
