<?php
/**
 * Registry for V2 schema integration contributors.
 *
 * @package SEOvaultSEO\Schema
 */

namespace SEOvaultSEO\Schema;

use SEOvaultSEO\Schema\Validation\ValueInspector;

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

/**
 * Collects and resolves trusted integration contributions.
 */
class SchemaIntegrationRegistry {

	const SOURCE = 'integration';

	/**
	 * Cached contributor instances for the request.
	 *
	 * @var SchemaIntegrationContributor[]|null
	 */
	private static $contributors = null;

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

	/**
	 * Registered contributors (built-in + filter).
	 *
	 * Filter: seovaultseo_schema_integration_contributors
	 *
	 * @return SchemaIntegrationContributor[]
	 */
	public static function get_contributors() {
		if ( null !== self::$contributors ) {
			return self::$contributors;
		}

		$built_in = array();

		/**
		 * Register schema integration contributors.
		 *
		 * Each item must implement SchemaIntegrationContributor.
		 *
		 * @param array $contributors Contributor instances.
		 */
		$filtered = apply_filters( 'seovaultseo_schema_integration_contributors', $built_in );
		if ( ! is_array( $filtered ) ) {
			$filtered = $built_in;
		}

		$clean = array();
		foreach ( $filtered as $contributor ) {
			if ( $contributor instanceof SchemaIntegrationContributor ) {
				$clean[] = $contributor;
			}
		}

		self::$contributors = $clean;
		return self::$contributors;
	}

	/**
	 * Collect tagged schemas from all contributors.
	 *
	 * @param \WP_Post|\WP_Term|\WP_User|null $object Render object.
	 * @return array<int,array{source:string,source_ref:string,schema:array}>
	 */
	public static function collect_tagged_schemas( $object = null ) {
		$tagged = array();

		foreach ( self::get_contributors() as $contributor ) {
			$rows = $contributor->collect_tagged_schemas( $object );
			if ( ! is_array( $rows ) ) {
				continue;
			}
			foreach ( $rows as $row ) {
				$normalized = self::normalize_tagged_row( $row, $contributor );
				if ( null !== $normalized ) {
					$tagged[] = $normalized;
				}
			}
		}

		return $tagged;
	}

	/**
	 * Union of types owned by active integrations for this object.
	 *
	 * @param \WP_Post|\WP_Term|\WP_User|null $object Render object.
	 * @return string[]
	 */
	public static function owned_types( $object = null ) {
		$owned = array();
		foreach ( self::get_contributors() as $contributor ) {
			$types = $contributor->owned_types( $object );
			if ( ! is_array( $types ) ) {
				continue;
			}
			foreach ( $types as $type ) {
				$type = is_string( $type ) ? trim( $type ) : '';
				if ( '' !== $type ) {
					$owned[ $type ] = true;
				}
			}
		}
		return array_keys( $owned );
	}

	/**
	 * Whether any registered contributor trusts this type/node.
	 *
	 * @param string $type    Schema type.
	 * @param array  $node    Sanitized node.
	 * @param array  $context Gate context.
	 * @return bool
	 */
	public static function is_trusted_type( $type, array $node, array $context = array() ) {
		$type = is_string( $type ) ? trim( $type ) : '';
		if ( '' === $type ) {
			return false;
		}

		foreach ( self::get_contributors() as $contributor ) {
			if ( $contributor->is_trusted_type( $type, $node, $context ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Whether a tagged contribution should be suppressed because an integration owns the type.
	 *
	 * Custom and integration sources are never suppressed here.
	 *
	 * @param array  $tagged Tagged contribution.
	 * @param string[] $owned_types Owned types.
	 * @return bool
	 */
	public static function should_suppress_owned_type( array $tagged, array $owned_types ) {
		if ( empty( $owned_types ) ) {
			return false;
		}

		$source = isset( $tagged['source'] ) ? (string) $tagged['source'] : '';
		if ( in_array( $source, array( self::SOURCE, 'custom' ), true ) ) {
			return false;
		}

		$types = self::contribution_types( isset( $tagged['schema'] ) ? $tagged['schema'] : null );
		if ( empty( $types ) ) {
			return false;
		}

		foreach ( $types as $type ) {
			if ( in_array( $type, $owned_types, true ) ) {
				return true;
			}
			// ProductGroup is owned whenever Product is owned by commerce integrations.
			if ( 'ProductGroup' === $type && in_array( 'Product', $owned_types, true ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Extract Schema.org types from a schema config or raw node.
	 *
	 * @param mixed $schema Schema payload.
	 * @return string[]
	 */
	public static function contribution_types( $schema ) {
		if ( ! is_array( $schema ) ) {
			return array();
		}

		if ( isset( $schema['@type'] ) || isset( $schema['@graph'] ) || isset( $schema['@id'] ) ) {
			if ( isset( $schema['@graph'] ) && is_array( $schema['@graph'] ) ) {
				$types = array();
				foreach ( $schema['@graph'] as $node ) {
					if ( is_array( $node ) ) {
						$types = array_merge( $types, ValueInspector::get_types( $node ) );
					}
				}
				return array_values( array_unique( $types ) );
			}
			return ValueInspector::get_types( $schema );
		}

		if ( ! empty( $schema['_v2_emit_type'] ) && is_string( $schema['_v2_emit_type'] ) ) {
			return array( trim( $schema['_v2_emit_type'] ) );
		}

		if ( isset( $schema['type'] ) && is_string( $schema['type'] ) && '' !== trim( $schema['type'] ) ) {
			return array( trim( $schema['type'] ) );
		}

		if ( isset( $schema['fields'] ) && is_array( $schema['fields'] ) && isset( $schema['fields']['@type'] ) ) {
			$fields = $schema['fields'];
			$fields['@type'] = $schema['fields']['@type'];
			return ValueInspector::get_types( $fields );
		}

		return array();
	}

	/**
	 * Normalize and force integration source metadata.
	 *
	 * @param mixed                           $row         Raw row.
	 * @param SchemaIntegrationContributor    $contributor Contributor.
	 * @return array{source:string,source_ref:string,schema:array}|null
	 */
	private static function normalize_tagged_row( $row, SchemaIntegrationContributor $contributor ) {
		if ( ! is_array( $row ) ) {
			return null;
		}

		$schema = isset( $row['schema'] ) && is_array( $row['schema'] ) ? $row['schema'] : null;
		if ( null === $schema || empty( $schema ) ) {
			return null;
		}

		$integration_id = sanitize_key( (string) $contributor->get_integration_id() );
		if ( '' === $integration_id ) {
			$integration_id = 'unknown';
		}

		$ref = isset( $row['source_ref'] ) ? (string) $row['source_ref'] : '';
		$ref = SchemaSourceDescriptor::normalize_source_ref( $ref );
		if ( '' === $ref ) {
			$ref = self::SOURCE . ':' . $integration_id;
		} elseif ( 0 !== strpos( $ref, self::SOURCE . ':' ) ) {
			$ref = self::SOURCE . ':' . $integration_id . ':' . ltrim( $ref, ':' );
		}

		return array(
			'source'     => self::SOURCE,
			'source_ref' => $ref,
			'schema'     => $schema,
		);
	}
}
