<?php
/**
 * V2 specialized emission gate (sanitize + readiness + withhold).
 *
 * Prevents incomplete specialized entities from entering the public graph when
 * the V2 specialized-readiness capability is enabled.
 *
 * @package SEOvaultSEO\Schema
 */

namespace SEOvaultSEO\Schema;

use SEOvaultSEO\Schema\Readiness\SchemaReadinessEvaluator;
use SEOvaultSEO\Schema\Readiness\SchemaReadinessIssue;
use SEOvaultSEO\Schema\Readiness\SchemaReadinessResult;
use SEOvaultSEO\Schema\Validation\ValueInspector;

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

/**
 * Decides whether a specialized candidate may emit under V2 rules.
 */
class SchemaSpecializedEmissionGate {

	/**
	 * Decide emit / withhold / skip for one rendered node.
	 *
	 * @param array  $node       Rendered candidate node.
	 * @param string $source     Source kind.
	 * @param array  $context    Optional evaluation context (`now`, `source_ref`).
	 * @return array{
	 *   action: string,
	 *   node: array,
	 *   withheld: array|null,
	 *   improvements: array<int,array>,
	 *   readiness: array|null
	 * }
	 */
	public static function decide( array $node, $source = '', array $context = array() ) {
		$source = is_string( $source ) ? $source : '';
		$types  = ValueInspector::get_types( $node );
		$type   = ! empty( $types ) ? (string) $types[0] : '';

		if ( '' === $type ) {
			$withheld = self::withheld_descriptor(
				$node,
				$source,
				'Thing',
				'missing_type',
				array(
					array(
						'code'       => 'specialized.type.missing',
						'kind'       => SchemaReadinessIssue::KIND_BLOCKER,
						'field_key'  => 'type',
						'action_key' => 'choose_supported_type',
						'path'       => '',
					),
				)
			);
			return array(
				'action'       => 'withhold',
				'node'         => array(),
				'withheld'     => $withheld,
				'improvements' => array(),
				'readiness'    => null,
			);
		}

		// Foundational types are handled by FoundationalContextCollector readiness.
		if ( SchemaTypePolicyRegistry::is_foundational( $type ) ) {
			return self::skip_decision( $node );
		}

		// Catalogue specialized types and unknown extension payloads both pass through
		// readiness so incomplete or hostile contributors cannot enter the V2 graph.
		$sanitized = SchemaSanitizer::sanitize_fields( $node );
		$sanitized = SchemaSanitizer::normalize_datetime_fields( $sanitized, $type );
		if ( empty( $sanitized ) ) {
			$withheld = self::withheld_descriptor(
				$node,
				$source,
				$type,
				'empty_after_sanitize',
				array(
					array(
						'code'       => 'specialized.sanitize.empty',
						'kind'       => SchemaReadinessIssue::KIND_BLOCKER,
						'field_key'  => 'payload',
						'action_key' => 'provide_valid_schema_fields',
						'path'       => '',
					),
				)
			);
			return array(
				'action'       => 'withhold',
				'node'         => array(),
				'withheld'     => $withheld,
				'improvements' => array(),
				'readiness'    => null,
			);
		}

		if ( ! isset( $sanitized['@type'] ) ) {
			$sanitized['@type'] = $type;
		}

		$eval_context = array();
		if ( isset( $context['now'] ) ) {
			$eval_context['now'] = (int) $context['now'];
		}

		$readiness    = SchemaReadinessEvaluator::evaluate( $type, $sanitized, $eval_context );
		$improvements = self::improvement_rows( $readiness, $type, $source, $sanitized );

		$trusted = self::is_trusted_integration( $source, $type, $sanitized, $context );
		if ( $trusted || $readiness->is_ready() ) {
			return array(
				'action'       => 'emit',
				'node'         => $sanitized,
				'withheld'     => null,
				'improvements' => $improvements,
				'readiness'    => $readiness->to_array(),
			);
		}

		$blockers = array();
		foreach ( $readiness->get_blockers() as $issue ) {
			$blockers[] = $issue->to_array();
		}

		$withheld = self::withheld_descriptor(
			$sanitized,
			$source,
			$type,
			'specialized_not_ready',
			$blockers
		);

		return array(
			'action'       => 'withhold',
			'node'         => array(),
			'withheld'     => $withheld,
			'improvements' => $improvements,
			'readiness'    => $readiness->to_array(),
		);
	}

	/**
	 * Whether an integration contribution may bypass readiness blockers.
	 *
	 * Extensions never qualify. Integrations must opt in through the documented filter.
	 *
	 * Filter: seovaultseo_schema_trusted_integration
	 *
	 * @param string $source  Source kind.
	 * @param string $type    Schema type.
	 * @param array  $node    Sanitized node.
	 * @param array  $context Context.
	 * @return bool
	 */
	public static function is_trusted_integration( $source, $type, array $node, array $context = array() ) {
		$source = is_string( $source ) ? $source : '';
		if ( 'extension' === $source ) {
			return false;
		}
		if ( 'integration' !== $source ) {
			return false;
		}

		$trusted = false;
		if ( function_exists( 'apply_filters' ) ) {
			/**
			 * Whether an integration-sourced specialized entity may bypass readiness blockers.
			 *
			 * Sanitization still applies. Extensions cannot use this path.
			 *
			 * @param bool   $trusted Whether trusted.
			 * @param string $type    Schema type.
			 * @param array  $node    Sanitized node.
			 * @param array  $context Gate context.
			 */
			$trusted = (bool) apply_filters( 'seovaultseo_schema_trusted_integration', $trusted, $type, $node, $context );
		}

		if ( ! $trusted ) {
			$trusted = SchemaIntegrationRegistry::is_trusted_type( $type, $node, $context );
		}

		return $trusted;
	}

	/**
	 * Build a skip decision for foundational or non-gated nodes.
	 *
	 * @param array $node Node.
	 * @return array
	 */
	private static function skip_decision( array $node ) {
		return array(
			'action'       => 'skip',
			'node'         => $node,
			'withheld'     => null,
			'improvements' => array(),
			'readiness'    => null,
		);
	}

	/**
	 * Build a withheld descriptor with field-level setup diagnostics.
	 *
	 * @param array  $node     Node used for id/type.
	 * @param string $source   Source kind.
	 * @param string $type     Type.
	 * @param string $reason   Machine reason.
	 * @param array  $blockers Blocker arrays.
	 * @return array
	 */
	private static function withheld_descriptor( array $node, $source, $type, $reason, array $blockers ) {
		$descriptor                = SchemaBuildResult::descriptor_from_node( $node, $source, 0 );
		$descriptor['emitted']     = false;
		$descriptor['withheld']    = true;
		$descriptor['status']      = SchemaSourceDescriptor::STATUS_REJECTED;
		$descriptor['reason']      = sanitize_key( (string) $reason );
		$descriptor['type']        = '' !== $type ? $type : ( $descriptor['type'] ?? 'Thing' );
		$descriptor['diagnostics'] = array_values( $blockers );
		$descriptor['setup']       = array_values( $blockers );

		return $descriptor;
	}

	/**
	 * Map readiness improvements into build-result improvement rows.
	 *
	 * @param SchemaReadinessResult $readiness Readiness result.
	 * @param string                $type      Type.
	 * @param string                $source    Source.
	 * @param array                 $node      Node.
	 * @return array<int,array>
	 */
	private static function improvement_rows( SchemaReadinessResult $readiness, $type, $source, array $node ) {
		$rows = array();
		foreach ( $readiness->get_improvements() as $issue ) {
			$row              = $issue->to_array();
			$row['type']      = (string) $type;
			$row['source']    = (string) $source;
			$row['entity_id'] = ValueInspector::get_id( $node );
			$rows[]           = $row;
		}
		return $rows;
	}
}
