<?php
/**
 * Documented public Schema Architecture V2 extension API.
 *
 * This class is the stable catalog of filters/interfaces extensions may use.
 * All returns are validated; hostile payloads cannot bypass sanitization,
 * readiness, or capability checks.
 *
 * @package SEOvaultSEO\Schema
 */

namespace SEOvaultSEO\Schema;

use SEOvaultSEO\Schema\Readiness\SchemaReadinessResult;

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

/**
 * Extension API catalog and sanitizers for documented filters.
 */
class SchemaExtensionApi {

	const FILTER_TYPE_POLICY          = 'seovaultseo_schema_type_policy_registry';
	const FILTER_EXTENSION_CONTRIBUTORS = 'seovaultseo_schema_extension_contributors';
	const FILTER_SET_SCHEMA           = 'seovaultseo_set_schema';
	const FILTER_ALLOWED_TYPES        = 'seovaultseo_schema_allowed_types';
	const FILTER_READINESS_RESULT     = 'seovaultseo_schema_readiness_result';
	const FILTER_PRESENTATION_STRINGS = 'seovaultseo_schema_validation_presentation_strings';
	const FILTER_INTEGRATION_CONTRIBUTORS = 'seovaultseo_schema_integration_contributors';
	const FILTER_TRUSTED_INTEGRATION  = 'seovaultseo_schema_trusted_integration';

	/**
	 * Documented public filters and their contracts.
	 *
	 * @return array<string,array{purpose:string,return:string,notes:string}>
	 */
	public static function documented_filters() {
		return array(
			self::FILTER_TYPE_POLICY               => array(
				'purpose' => 'Register additional type-policy catalogue entries.',
				'return'  => 'array<string,array> sanitized by SchemaTypePolicyRegistry',
				'notes'   => 'Built-in types cannot be replaced. Invalid categories/sources are dropped.',
			),
			self::FILTER_EXTENSION_CONTRIBUTORS    => array(
				'purpose' => 'Register SchemaExtensionContributor instances.',
				'return'  => 'SchemaExtensionContributor[]',
				'notes'   => 'Preferred contribution path. Source is forced to extension; readiness always applies.',
			),
			self::FILTER_SET_SCHEMA                => array(
				'purpose' => 'Legacy contribution of schema configs/nodes for the current object.',
				'return'  => 'array of schema configs or tagged {source,source_ref,schema} rows',
				'notes'   => 'Non-array returns are rejected. Claimed non-extension sources are dropped. Prefer SchemaExtensionContributor.',
			),
			self::FILTER_ALLOWED_TYPES             => array(
				'purpose' => 'Extend SchemaSanitizer allowed @type list.',
				'return'  => 'string[] of Schema.org type names',
				'notes'   => 'Non-string / empty / oversized values stripped. Does not grant capability bypass.',
			),
			self::FILTER_READINESS_RESULT          => array(
				'purpose' => 'Adjust readiness evaluation results for a type.',
				'return'  => 'SchemaReadinessResult instance only',
				'notes'   => 'Invalid returns are ignored. Cannot mark extension sources as trusted integrations.',
			),
			self::FILTER_PRESENTATION_STRINGS      => array(
				'purpose' => 'Override Schema Health presenter title/message/action copy.',
				'return'  => 'array<code,array{0:title,1:message,2:action}>',
				'notes'   => 'Values are sanitized to plain text. Display-only; no emission change.',
			),
			self::FILTER_INTEGRATION_CONTRIBUTORS  => array(
				'purpose' => 'Register trusted SchemaIntegrationContributor instances (objective domains).',
				'return'  => 'SchemaIntegrationContributor[]',
				'notes'   => 'Not for ordinary extensions. Ownership/suppress rules apply.',
			),
			self::FILTER_TRUSTED_INTEGRATION       => array(
				'purpose' => 'Opt an integration-sourced type into readiness bypass.',
				'return'  => 'bool',
				'notes'   => 'Extensions never qualify even if this filter returns true.',
			),
		);
	}

	/**
	 * Short developer-facing hook list for the Schema settings Advanced section.
	 *
	 * @return string[]
	 */
	public static function documented_filter_names() {
		return array_keys( self::documented_filters() );
	}

	/**
	 * Sanitize allowed-types filter return.
	 *
	 * @param mixed $types Filter return.
	 * @return string[]
	 */
	public static function sanitize_allowed_types( $types ) {
		if ( ! is_array( $types ) ) {
			return array();
		}

		$clean = array();
		foreach ( $types as $type ) {
			if ( ! is_string( $type ) ) {
				continue;
			}
			$type = trim( sanitize_text_field( $type ) );
			if ( '' === $type || strlen( $type ) > 80 ) {
				continue;
			}
			if ( ! preg_match( '/^[A-Za-z][A-Za-z0-9]*$/', $type ) ) {
				continue;
			}
			$clean[ $type ] = $type;
		}

		return array_values( $clean );
	}

	/**
	 * Sanitize presentation-string catalog filter return.
	 *
	 * @param mixed $catalog Filter return.
	 * @return array<string,array{0:string,1:string,2:string}>
	 */
	public static function sanitize_presentation_catalog( $catalog ) {
		if ( ! is_array( $catalog ) ) {
			return array();
		}

		$clean = array();
		foreach ( $catalog as $code => $row ) {
			if ( ! is_string( $code ) || '' === $code || ! is_array( $row ) ) {
				continue;
			}
			$code = strtolower( preg_replace( '/[^a-z0-9._\-]/i', '', $code ) );
			if ( '' === $code || strlen( $code ) > 120 ) {
				continue;
			}
			$title   = isset( $row[0] ) ? self::plain_text( $row[0] ) : '';
			$message = isset( $row[1] ) ? self::plain_text( $row[1] ) : '';
			$action  = isset( $row[2] ) ? self::plain_text( $row[2] ) : '';
			if ( '' === $title && '' === $message && '' === $action ) {
				continue;
			}
			$clean[ $code ] = array( $title, $message, $action );
		}

		return $clean;
	}

	/**
	 * Accept only SchemaReadinessResult from the readiness filter.
	 *
	 * @param mixed                  $filtered Filter return.
	 * @param SchemaReadinessResult  $fallback Built-in result.
	 * @return SchemaReadinessResult
	 */
	public static function sanitize_readiness_result( $filtered, SchemaReadinessResult $fallback ) {
		if ( $filtered instanceof SchemaReadinessResult ) {
			return $filtered;
		}
		return $fallback;
	}

	/**
	 * @param mixed $value Raw value.
	 * @return string
	 */
	private static function plain_text( $value ) {
		if ( ! is_string( $value ) && ! is_numeric( $value ) ) {
			return '';
		}
		$text = wp_strip_all_tags( (string) $value );
		$text = sanitize_text_field( $text );
		if ( strlen( $text ) > 500 ) {
			$text = substr( $text, 0, 500 );
		}
		return $text;
	}
}
