<?php
/**
 * Loader and generator-closure helpers for the bundled Schema.org vocabulary.
 *
 * @package SEOvaultSEO\Schema\Validation\Data
 */

namespace SEOvaultSEO\Schema\Validation\Data;

use SEOvaultSEO\Schema\SchemaSanitizer;
use SEOvaultSEO\Schema\SchemaUtils;

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

/**
 * Provides versioned vocabulary access and generator term collection.
 */
class SchemaOrgVocabularyCatalog {

	/**
	 * Cached vocabulary payload.
	 *
	 * @var array|null
	 */
	private static $cached = null;

	/**
	 * Load the bundled vocabulary data file.
	 *
	 * @return array{version:string,source_url:string,checked_at:string,types:string[],properties:string[]}
	 */
	public static function load() {
		if ( null !== self::$cached ) {
			return self::$cached;
		}

		$data = require __DIR__ . '/SchemaOrgVocabulary.php';
		self::$cached = is_array( $data ) ? $data : array(
			'version'    => '',
			'source_url' => '',
			'checked_at' => '',
			'types'      => array(),
			'properties' => array(),
		);

		return self::$cached;
	}

	/**
	 * Reset cache (tests).
	 *
	 * @return void
	 */
	public static function reset_cache() {
		self::$cached = null;
	}

	/**
	 * Metadata keys for review/versioning.
	 *
	 * @return array{version:string,source_url:string,checked_at:string}
	 */
	public static function meta() {
		$data = self::load();
		return array(
			'version'    => (string) ( $data['version'] ?? '' ),
			'source_url' => (string) ( $data['source_url'] ?? '' ),
			'checked_at' => (string) ( $data['checked_at'] ?? '' ),
		);
	}

	/**
	 * Known type names keyed for isset lookup.
	 *
	 * @return array<string,bool>
	 */
	public static function known_types_map() {
		$data = self::load();
		$types = isset( $data['types'] ) && is_array( $data['types'] ) ? $data['types'] : array();
		return array_fill_keys( array_map( 'strval', $types ), true );
	}

	/**
	 * Known property names keyed for isset lookup.
	 *
	 * @return array<string,bool>
	 */
	public static function known_properties_map() {
		$data = self::load();
		$props = isset( $data['properties'] ) && is_array( $data['properties'] ) ? $data['properties'] : array();
		return array_fill_keys( array_map( 'strval', $props ), true );
	}

	/**
	 * Whether a type is in the bundled subset.
	 *
	 * @param string $type Type name.
	 * @return bool
	 */
	public static function is_known_type( $type ) {
		$map = self::known_types_map();
		return isset( $map[ (string) $type ] );
	}

	/**
	 * Whether a property is in the bundled subset.
	 *
	 * @param string $property Property name.
	 * @return bool
	 */
	public static function is_known_property( $property ) {
		$map = self::known_properties_map();
		return isset( $map[ (string) $property ] );
	}

	/**
	 * Collect every type/property emitted by core generators (transitive nested fields).
	 *
	 * @return array{types:string[],properties:string[]}
	 */
	public static function collect_generator_terms() {
		$types = array();
		$props = array();

		foreach ( SchemaUtils::get_default_schema_types() as $type_name => $class ) {
			$types[ (string) $type_name ] = true;
			if ( ! is_string( $class ) || ! class_exists( $class ) ) {
				continue;
			}
			$instance = new $class();
			if ( ! method_exists( $instance, 'get' ) ) {
				continue;
			}
			$fields = $instance->get();
			if ( is_array( $fields ) ) {
				self::collect_from_fields( $fields, $types, $props );
			}
		}

		// Nested / integration types explicitly allowed by sanitizer.
		foreach ( self::sanitizer_allowed_types() as $type ) {
			$types[ $type ] = true;
		}

		// WooCommerce integration emissions (stable set).
		foreach ( array( 'Book', 'MerchantReturnPolicy', 'OnlineStore', 'ProductGroup', 'Brand', 'Offer', 'AggregateRating', 'Product' ) as $type ) {
			$types[ $type ] = true;
		}
		foreach ( array( 'merchantReturnLink', 'hasMerchantReturnPolicy', 'mpn', 'isbn', 'gtin', 'gtin8', 'gtin12', 'gtin13', 'gtin14', 'brand', 'sku', 'offers', 'aggregateRating' ) as $prop ) {
			$props[ $prop ] = true;
		}

		$types_out = array_keys( $types );
		$props_out = array_keys( $props );
		sort( $types_out, SORT_STRING );
		sort( $props_out, SORT_STRING );

		return array(
			'types'      => $types_out,
			'properties' => $props_out,
		);
	}

	/**
	 * Recursively collect ids and nested @type defaults from field definitions.
	 *
	 * @param array $fields Field list.
	 * @param array $types  Type set (by ref).
	 * @param array $props  Property set (by ref).
	 * @return void
	 */
	public static function collect_from_fields( array $fields, array &$types, array &$props ) {
		foreach ( $fields as $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}

			$id = isset( $field['id'] ) ? (string) $field['id'] : '';
			if ( '@type' === $id ) {
				$std = isset( $field['std'] ) ? (string) $field['std'] : '';
				if ( '' !== $std && 0 !== strpos( $std, '%' ) ) {
					$types[ $std ] = true;
				}
			} elseif ( '' !== $id && 0 !== strpos( $id, '@' ) && 'schema_name' !== $id ) {
				$props[ $id ] = true;
			}

			if ( isset( $field['fields'] ) && is_array( $field['fields'] ) ) {
				self::collect_from_fields( $field['fields'], $types, $props );
			}
		}
	}

	/**
	 * Types accepted by SchemaSanitizer (via reflection for invariant coverage).
	 *
	 * @return string[]
	 */
	private static function sanitizer_allowed_types() {
		if ( ! class_exists( SchemaSanitizer::class ) ) {
			return array();
		}

		try {
			$ref  = new \ReflectionClass( SchemaSanitizer::class );
			$prop = $ref->getProperty( 'allowed_types' );
			if ( PHP_VERSION_ID < 80100 ) {
				$prop->setAccessible( true );
			}
			$value = $prop->getValue();
			return is_array( $value ) ? array_map( 'strval', $value ) : array();
		} catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
			return array();
		}
	}
}
