<?php
/**
 * Niwa AI Blog Asset Renderer.
 *
 * Reads post meta (`_niwa_blog_css`, `_niwa_blog_progress_js`, `_niwa_blog_schemas`)
 * and outputs them on `wp_head` / `wp_footer` only on the singular page of that
 * specific post.
 *
 * Why: `wp_filter_post_kses()` strips `<style>` and `<script>` tags from
 * `post_content`, but keeps their inner text — which then leaks as raw text on
 * the frontend. Storing the payloads in post meta and re-rendering them via
 * head/footer hooks avoids that filter entirely.
 *
 * Storage contract:
 *   - `_niwa_blog_css`         — pure CSS body (no `<style>` tags).
 *   - `_niwa_blog_progress_js` — pure JS body (no `<script>` tags).
 *   - `_niwa_blog_schemas`     — array of pure JSON strings (no `<script>` wrappers).
 *
 * The renderer wraps each payload in the correct tag at output time.
 *
 * @package Niwa_AI
 * @since   0.13.126
 */

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

class Niwa_AI_Blog_Asset_Renderer {

	const META_CSS      = '_niwa_blog_css';
	const META_JS       = '_niwa_blog_progress_js';
	const META_SCHEMAS  = '_niwa_blog_schemas';

	/**
	 * Register WP hooks.
	 */
	public static function register_hooks() {
		add_action( 'wp_head', array( __CLASS__, 'render_head_assets' ), 20 );
		add_action( 'wp_footer', array( __CLASS__, 'render_footer_assets' ), 20 );
		add_action( 'save_post', array( 'Niwa_AI_Blog_Quality_Gate', 'invalidate_content_map' ), 20 );
		add_action( 'trashed_post', array( 'Niwa_AI_Blog_Quality_Gate', 'invalidate_content_map' ), 20 );
		add_action( 'untrashed_post', array( 'Niwa_AI_Blog_Quality_Gate', 'invalidate_content_map' ), 20 );
		add_action( 'deleted_post', array( 'Niwa_AI_Blog_Quality_Gate', 'invalidate_content_map' ), 20 );
	}

	/**
	 * Output CSS + JSON-LD schemas on `wp_head`.
	 */
	public static function render_head_assets() {
		$post_id = self::current_singular_post_id();
		if ( 0 === $post_id ) {
			return;
		}

		$css = get_post_meta( $post_id, self::META_CSS, true );
		if ( is_string( $css ) && '' !== trim( $css ) ) {
			echo "\n<style id=\"niwa-blog-css-" . (int) $post_id . "\">\n" . self::sanitize_css_for_output( $css ) . "\n</style>\n";
		}

		$schemas = get_post_meta( $post_id, self::META_SCHEMAS, true );
		if ( is_array( $schemas ) ) {
			foreach ( $schemas as $schema_json ) {
				if ( ! is_string( $schema_json ) || '' === trim( $schema_json ) ) {
					continue;
				}
				$sanitized = self::sanitize_json_for_output( $schema_json );
				if ( '' === $sanitized ) {
					continue;
				}
				echo '<script type="application/ld+json">' . $sanitized . "</script>\n";
			}
		}
	}

	/**
	 * Output progress bar JS on `wp_footer`.
	 */
	public static function render_footer_assets() {
		$post_id = self::current_singular_post_id();
		if ( 0 === $post_id ) {
			return;
		}

		$js = get_post_meta( $post_id, self::META_JS, true );
		if ( is_string( $js ) && '' !== trim( $js ) ) {
			echo "\n<script id=\"niwa-blog-js-" . (int) $post_id . "\">\n" . self::sanitize_js_for_output( $js ) . "\n</script>\n";
		}
	}

	/**
	 * Return the post ID if the current request is a singular blog post.
	 *
	 * @return int Post ID, or 0 if not applicable.
	 */
	private static function current_singular_post_id() {
		if ( ! is_singular( 'post' ) ) {
			return 0;
		}
		$post_id = get_queried_object_id();
		return is_numeric( $post_id ) ? (int) $post_id : 0;
	}

	/**
	 * Defensive CSS sanitization for output.
	 *
	 * Strips any literal `</style>` sequence to prevent breakout.
	 *
	 * @param string $css Raw CSS body.
	 * @return string
	 */
	private static function sanitize_css_for_output( $css ) {
		$css = (string) $css;
		$css = preg_replace( '#</\s*style\s*>#i', '', $css );
		return $css;
	}

	/**
	 * Defensive JS sanitization for output.
	 *
	 * Strips any literal `</script>` sequence to prevent breakout.
	 *
	 * @param string $js Raw JS body.
	 * @return string
	 */
	private static function sanitize_js_for_output( $js ) {
		$js = (string) $js;
		$js = preg_replace( '#</\s*script\s*>#i', '', $js );
		return $js;
	}

	/**
	 * Defensive JSON-LD sanitization.
	 *
	 * Strips any literal `</script>` sequence and validates the payload is JSON.
	 *
	 * @param string $json Raw JSON body.
	 * @return string
	 */
	private static function sanitize_json_for_output( $json ) {
		$json = (string) $json;
		$json = preg_replace( '#</\s*script\s*>#i', '', $json );

		$decoded = json_decode( $json, true );
		if ( null === $decoded && JSON_ERROR_NONE !== json_last_error() ) {
			return '';
		}
		return wp_json_encode( $decoded, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
	}
}
