<?php
/**
 * Schema engine — emits JSON-LD on every front-end page.
 *
 * @package WPLens
 */

namespace WPLens\Schema;

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

final class SchemaEngine {

	/**
	 * Cached post-meta key prefix for generated schema.
	 */
	public const META_KEY = '_wplens_schema_cache';

	public function register(): void {
		add_action( 'wp_head', array( $this, 'emit' ), 15 );
		// Invalidate cache when posts are saved.
		add_action( 'save_post', array( $this, 'invalidate_post_cache' ), 10, 1 );
	}

	public function emit(): void {
		$settings = \WPLens\Utils\Options::all();
		if ( empty( $settings['schema']['enabled'] ) ) {
			return;
		}

		$blocks = $this->build_blocks();
		if ( empty( $blocks ) ) {
			return;
		}

		// One @graph block — Google's preferred shape for multi-type emission on one page.
		$payload = array(
			'@context' => 'https://schema.org',
			'@graph'   => $blocks,
		);

		// IMPORTANT: omit JSON_UNESCAPED_SLASHES so that forward slashes ARE escaped
		// as \/ — this prevents any user-supplied content that happens to contain the
		// literal substring </script> from breaking out of the JSON-LD script tag.
		// JSON_UNESCAPED_UNICODE is fine to keep on; emojis / accented chars stay readable.
		$json = wp_json_encode( $payload, JSON_UNESCAPED_UNICODE );
		if ( false === $json ) {
			return;
		}

		echo "\n" . '<script type="application/ld+json" data-emitter="lens">' . "\n";
		// $json is the return value of wp_json_encode() — the canonical safe way to emit
		// JSON inside a <script> tag. With slashes escaped (above), there is no XSS surface.
		echo $json; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo "\n</script>\n";
	}

	/**
	 * Run all registered generators and return their JSON-LD blocks.
	 *
	 * @return array<int, array<string, mixed>>
	 */
	private function build_blocks(): array {
		$post     = is_singular() ? get_post() : null;
		$settings = \WPLens\Utils\Options::all();
		$disabled = (array) ( $settings['schema']['disabled_types'] ?? array() );

		// Cache: only post-specific generators get cached per-post.
		if ( $post instanceof \WP_Post ) {
			$cached = $this->read_post_cache( $post );
			if ( null !== $cached ) {
				return $cached;
			}
		}

		$blocks = array();
		foreach ( $this->generators() as $g ) {
			if ( in_array( $g->get_slug(), $disabled, true ) ) {
				continue;
			}
			if ( ! $g->applies( $post ) ) {
				continue;
			}
			$payload = $g->generate( $post );
			if ( is_array( $payload ) && ! empty( $payload ) ) {
				$blocks[] = $payload;
			}
		}

		if ( $post instanceof \WP_Post ) {
			$this->write_post_cache( $post, $blocks );
		}

		return $blocks;
	}

	/**
	 * Registered generators. Filterable.
	 *
	 * @return array<int, Generator>
	 */
	public function generators(): array {
		$list = array(
			new Generators\WebsiteGenerator(),
			new Generators\OrganizationGenerator(),
			new Generators\WebpageGenerator(),
			new Generators\BreadcrumbGenerator(),
			new Generators\ArticleGenerator(),
			new Generators\FaqGenerator(),
			new Generators\HowtoGenerator(),
			new Generators\SpeakableGenerator(),
			new Generators\ImageGenerator(),
			new Generators\VideoGenerator(),
		);

		/**
		 * Filter the active list of schema generators.
		 *
		 * @param array<int, Generator> $list
		 */
		return apply_filters( 'wplens/schema_generators', $list );
	}

	private function read_post_cache( \WP_Post $post ): ?array {
		$cached = get_post_meta( $post->ID, self::META_KEY, true );
		if ( ! is_array( $cached ) || ! isset( $cached['hash'], $cached['blocks'] ) ) {
			return null;
		}
		if ( $cached['hash'] !== $this->cache_hash( $post ) ) {
			return null;
		}
		return is_array( $cached['blocks'] ) ? $cached['blocks'] : null;
	}

	private function write_post_cache( \WP_Post $post, array $blocks ): void {
		update_post_meta(
			$post->ID,
			self::META_KEY,
			array(
				'hash'   => $this->cache_hash( $post ),
				'blocks' => $blocks,
				'time'   => time(),
			)
		);
	}

	public function invalidate_post_cache( int $post_id ): void {
		if ( wp_is_post_revision( $post_id ) ) {
			return;
		}
		delete_post_meta( $post_id, self::META_KEY );
	}

	private function cache_hash( \WP_Post $post ): string {
		$settings = \WPLens\Utils\Options::all();
		$signature = $post->post_modified_gmt
			. '|' . md5( wp_json_encode( $settings['schema'] ?? array() ) )
			. '|v1';
		return md5( $signature );
	}
}
