<?php
/**
 * Schema renderer and manager
 *
 * Coordinates output of all schema types on the page.
 * Handles schema selection, validation, and caching.
 *
 * @package BW_Schema
 * @since 3.0.0-babel
 */

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

class BW_Schema_Schema_Renderer {

	/**
	 * Array of schemas to output on this page
	 *
	 * @var BW_Schema_Schema_Base[]
	 */
	private static $schemas = array();

	/**
	 * Whether schemas have been collected
	 *
	 * @var bool
	 */
	private static $collected = false;

	/**
	 * Initialize renderer and register hooks
	 *
	 * @return void
	 */
	public static function init() {
		// Register hook to output schemas
		add_action( 'wp_head', array( __CLASS__, 'output_schemas' ), 1 );

		// Register hooks to collect schemas
		add_action( 'wp_head', array( __CLASS__, 'collect_schemas' ), 0 );
	}

	/**
	 * Collect all schemas for the current page
	 *
	 * Called before output_schemas() so all schema types can be gathered.
	 *
	 * @return void
	 */
	public static function collect_schemas() {
		if ( self::$collected ) {
			return;
		}

		// NOTE: Organization schema is NOT collected here. It is output by
		// BW_Schema_Core::output_schema_markup() via BW_Schema_Generator, which
		// is the full-featured org schema system (contact, hours, FAQ, reviews…).
		// Collecting it here too would emit duplicate Organization markup.

		// Collect singular post schema
		if ( is_singular() ) {
			try {
				global $post;

				// Per-post opt-out from the Solomon Schema metabox — skips this
				// post's own Article/Person schema (site-level markup unaffected)
				$post_schema_disabled = ( $post && '1' === get_post_meta( $post->ID, '_bw_schema_disable', true ) );

				// Team member pages get Person schema, not Article
				$team_post_type = class_exists( 'BW_Schema_Service_People' )
					? BW_Schema_Service_People::get_team_post_type()
					: '';

				if ( $post_schema_disabled ) {
					// Nothing collected for this post
				} elseif ( $team_post_type && is_singular( $team_post_type ) ) {
					$person_schema = new BW_Schema_Schema_Person( $post );
					self::add_schema( $person_schema );
				} else {
					// Article schema only for post types enabled in content settings
					$article_types = class_exists( 'BW_Schema_Service_Content' )
						? BW_Schema_Service_Content::get_article_post_types()
						: array( 'post', 'page' );

					if ( $post && in_array( get_post_type( $post ), $article_types, true ) ) {
						$article_schema = new BW_Schema_Schema_Article( $post );
						self::add_schema( $article_schema );
					}
				}
			} catch ( Throwable $e ) {
				error_log( 'BW Schema: Singular schema error - ' . $e->getMessage() );
			}
		}

		// Collect breadcrumb schema (on non-home pages, unless disabled)
		$breadcrumbs_enabled = ! class_exists( 'BW_Schema_Service_Content' )
			|| BW_Schema_Service_Content::is_breadcrumbs_enabled();

		if ( $breadcrumbs_enabled && ! is_home() && ! is_front_page() ) {
			try {
				$breadcrumb_schema = new BW_Schema_Schema_Breadcrumb();
				self::add_schema( $breadcrumb_schema );
			} catch ( Throwable $e ) {
				error_log( 'BW Schema: Breadcrumb schema error - ' . $e->getMessage() );
			}
		}

		// Collect location schema (on location pages if available)
		if ( is_singular( 'location' ) ) {
			try {
				global $post;
				$location_schema = new BW_Schema_Schema_LocalBusiness( $post );
				self::add_schema( $location_schema );
			} catch ( Throwable $e ) {
				error_log( 'BW Schema: Location schema error - ' . $e->getMessage() );
			}
		}

		// Allow custom schemas via filter
		try {
			$custom_schemas = apply_filters( 'bw_schema_schemas', array() );
			foreach ( $custom_schemas as $schema ) {
				self::add_schema( $schema );
			}
		} catch ( Throwable $e ) {
			error_log( 'BW Schema: Custom schema error - ' . $e->getMessage() );
		}

		self::$collected = true;
	}

	/**
	 * Add a schema to the output queue
	 *
	 * @param BW_Schema_Schema_Base $schema Schema instance
	 * @return void
	 */
	public static function add_schema( BW_Schema_Schema_Base $schema ) {
		// Only add valid schemas
		if ( $schema->is_valid() ) {
			self::$schemas[] = $schema;
		}
	}

	/**
	 * Get all collected schemas
	 *
	 * @return BW_Schema_Schema_Base[] Array of schema instances
	 */
	public static function get_schemas() {
		if ( ! self::$collected ) {
			self::collect_schemas();
		}

		return self::$schemas;
	}

	/**
	 * Get schemas as array (for JSON output)
	 *
	 * @return array Schemas as arrays
	 */
	public static function get_schemas_array() {
		$schemas = array();

		foreach ( self::get_schemas() as $schema ) {
			$schemas[] = $schema->to_array();
		}

		return $schemas;
	}

	/**
	 * Output all schemas as script tags
	 *
	 * Called by wp_head hook.
	 *
	 * @return void
	 */
	public static function output_schemas() {
		$schemas = self::get_schemas();

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

		// Check if we should use graph format (multiple schemas)
		if ( count( $schemas ) > 1 ) {
			self::output_graph( $schemas );
		} else {
			// Single schema, output directly
			foreach ( $schemas as $schema ) {
				// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
				echo $schema->to_html() . "\n";
			}
		}
	}

	/**
	 * Output multiple schemas as a graph
	 *
	 * This uses @graph to combine multiple schemas into one script tag.
	 *
	 * @param BW_Schema_Schema_Base[] $schemas Array of schema instances
	 * @return void
	 */
	private static function output_graph( $schemas ) {
		$graph = array(
			'@context' => 'https://schema.org',
			'@graph'   => array(),
		);

		foreach ( $schemas as $schema ) {
			$graph['@graph'][] = $schema->to_array();
		}

		$json = wp_json_encode( $graph, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );

		printf(
			"<script type=\"application/ld+json\">%s</script>\n",
			$json
		);
	}

	/**
	 * Clear collected schemas (useful for testing)
	 *
	 * @return void
	 */
	public static function clear() {
		self::$schemas   = array();
		self::$collected = false;
	}

	/**
	 * Get count of collected schemas
	 *
	 * @return int Number of schemas
	 */
	public static function count() {
		return count( self::get_schemas() );
	}

	/**
	 * Check if a schema type is present
	 *
	 * @param string $type Schema.org type (e.g., 'Organization', 'Article')
	 * @return bool True if schema type is present
	 */
	public static function has_type( $type ) {
		foreach ( self::get_schemas() as $schema ) {
			if ( $schema->get_type() === $type ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Get debug information
	 *
	 * Useful for troubleshooting schema output.
	 *
	 * @return array Debug data
	 */
	public static function get_debug_info() {
		$schemas = self::get_schemas();
		$debug   = array(
			'count'   => count( $schemas ),
			'schemas' => array(),
		);

		foreach ( $schemas as $schema ) {
			$errors = $schema->validate();
			$debug['schemas'][] = array(
				'type'   => $schema->get_type(),
				'valid'  => empty( $errors ),
				'errors' => $errors,
			);
		}

		return $debug;
	}
}
