<?php
/**
 * Targeted schema template collector (Architecture V2 / Phase 50).
 *
 * Resolves mapped variables against the preview/target object, then evaluates
 * readiness before contributing specialized entities. Incomplete or empty
 * resolved instances are withheld and never sprayed onto matching pages.
 *
 * @package SEOvaultSEO\Schema
 */

namespace SEOvaultSEO\Schema;

use SEOvaultSEO\Schema\Readiness\SchemaReadinessEvaluator;
use SEOvaultSEO\Schema\Readiness\SchemaReadinessIssue;

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

/**
 * Collects ready template contributions for a render object.
 */
class SchemaTemplateCollector {

	const SOURCE = 'template';

	/**
	 * Collect matching published templates for an object.
	 *
	 * @param \WP_Post|\WP_Term|\WP_User|null $object Target object.
	 * @return array{
	 *   tagged: array<int,array{source:string,source_ref:string,schema:array}>,
	 *   withheld: array<int,array>,
	 *   improvements: array<int,array>
	 * }
	 */
	public static function collect_for_object( $object = null ) {
		$empty = array(
			'tagged'       => array(),
			'withheld'     => array(),
			'improvements' => array(),
		);

		if ( is_admin() && ! wp_doing_ajax() && ! $object ) {
			return $empty;
		}

		$templates = DisplayConditions::get_matching_templates( $object );
		if ( empty( $templates ) ) {
			return $empty;
		}

		$renderer     = new SchemaRenderer( $object );
		$tagged       = array();
		$withheld     = array();
		$improvements = array();

		foreach ( $templates as $template ) {
			if ( ! is_array( $template ) ) {
				continue;
			}

			$template_id = isset( $template['template_id'] ) ? (int) $template['template_id'] : 0;
			$doc         = $template_id > 0 ? SchemaTemplate::get_document( $template_id ) : SchemaTemplate::normalize_document( $template, 0 );
			if ( empty( $doc ) ) {
				continue;
			}

			// Draft / non-publish templates must never contribute (defense in depth;
			// DisplayConditions already queries publish only).
			if ( $template_id > 0 ) {
				$status = get_post_status( $template_id );
				if ( 'publish' !== $status ) {
					continue;
				}
			}

			$type   = (string) ( $doc['type'] ?? 'Thing' );
			$fields = isset( $doc['fields'] ) && is_array( $doc['fields'] ) ? $doc['fields'] : array();
			if ( empty( $fields ) ) {
				$fields = SchemaUtils::parse_fields( $doc );
			}

			// Sanitize first so Hidden defaults (often %variable% tokens) are present,
			// then resolve against the target object before readiness.
			if ( SchemaInstanceFieldSanitizer::is_guided_or_manual_product_type( $type ) ) {
				$sanitized = SchemaInstanceFieldSanitizer::sanitize( $type, $fields );
				$fields    = isset( $sanitized['fields'] ) && is_array( $sanitized['fields'] )
					? $sanitized['fields']
					: array();
			} else {
				$fields = SchemaSanitizer::sanitize_fields( $fields );
				$fields = SchemaSanitizer::normalize_datetime_fields( $fields, $type );
			}

			$resolved   = self::resolve_fields( $fields, $renderer );
			$source_ref = SchemaTemplate::stable_source_ref( $template_id > 0 ? $template_id : 0 );
			if ( '' === $source_ref && $template_id <= 0 ) {
				$source_ref = 'template:anonymous';
			}
			$source_id = $template_id > 0
				? SchemaTemplate::stable_source_id( $template_id )
				: $source_ref;

			$unresolved = self::has_unresolved_mapped_variables( $resolved );
			$readiness  = null;

			if ( SchemaTypePolicyRegistry::is_specialized( $type ) ) {
				$readiness = SchemaReadinessEvaluator::evaluate( $type, $resolved );
				if ( ! $readiness->is_ready() || $unresolved ) {
					$reason = $unresolved && $readiness->is_ready()
						? 'template_variables_unresolved'
						: 'template_not_ready';
					$withheld[] = self::withheld_descriptor(
						$type,
						$source_ref,
						$source_id,
						$reason,
						$readiness,
						$resolved
					);
					foreach ( self::improvement_rows( $readiness, $type, $source_ref ) as $row ) {
						$improvements[] = $row;
					}
					continue;
				}
				foreach ( self::improvement_rows( $readiness, $type, $source_ref ) as $row ) {
					$improvements[] = $row;
				}
			} elseif ( $unresolved ) {
				// Foundational / unknown catalogue types: still withhold when mapped
				// variables collapse to empty / remain unresolved.
				$withheld[] = self::withheld_descriptor(
					$type,
					$source_ref,
					$source_id,
					'template_variables_unresolved',
					null,
					$resolved
				);
				continue;
			}

			$tagged[] = array(
				'source'     => self::SOURCE,
				'source_ref' => $source_ref,
				'schema'     => array(
					'title'      => $doc['metadata']['title'] ?? $doc['title'] ?? $type,
					'type'       => $type,
					'enabled'    => true,
					'show_on'    => array(
						'rules'        => array( 'basic-global' ),
						'specific'     => array(),
						'specificText' => array(),
					),
					'fields'     => $resolved,
					'_v2_source' => array(
						'kind'      => self::SOURCE,
						'source_id' => $source_id,
						'source_ref'=> $source_ref,
						'template_id' => $template_id,
					),
				),
			);
		}

		return array(
			'tagged'       => $tagged,
			'withheld'     => $withheld,
			'improvements' => $improvements,
		);
	}

	/**
	 * Resolve %variable% placeholders in a field tree against a renderer.
	 *
	 * @param array          $fields   Field values.
	 * @param SchemaRenderer $renderer Renderer bound to the target object.
	 * @return array
	 */
	public static function resolve_fields( array $fields, SchemaRenderer $renderer ) {
		$resolved = $fields;
		$renderer->render( $resolved );
		return is_array( $resolved ) ? $resolved : array();
	}

	/**
	 * Whether resolved fields still contain mapped %variable% tokens or
	 * empty strings that originated as mapped variables.
	 *
	 * Pure unresolved tokens are never meaningful for readiness; this helper
	 * catches cases where a required field was only a variable and resolved empty.
	 *
	 * @param mixed $value Resolved value tree.
	 * @return bool
	 */
	public static function has_unresolved_mapped_variables( $value ) {
		if ( is_string( $value ) ) {
			if ( preg_match( '/%[a-z0-9_.]+%/i', $value ) ) {
				return true;
			}
			return false;
		}
		if ( ! is_array( $value ) ) {
			return false;
		}
		foreach ( $value as $item ) {
			if ( self::has_unresolved_mapped_variables( $item ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Build a withheld descriptor for a template instance.
	 *
	 * @param string                              $type       Schema type.
	 * @param string                              $source_ref Source ref.
	 * @param string                              $source_id  Stable source id.
	 * @param string                              $reason     Withhold reason.
	 * @param \SEOvaultSEO\Schema\Readiness\SchemaReadinessResult|null $readiness Readiness result.
	 * @param array                               $fields     Resolved fields.
	 * @return array
	 */
	private static function withheld_descriptor( $type, $source_ref, $source_id, $reason, $readiness, array $fields ) {
		$diagnostics = array();
		if ( $readiness ) {
			foreach ( $readiness->get_blockers() as $issue ) {
				$diagnostics[] = $issue->to_array();
			}
		}
		if ( 'template_variables_unresolved' === $reason && empty( $diagnostics ) ) {
			$diagnostics[] = array(
				'code'       => 'template.variables.unresolved',
				'kind'       => SchemaReadinessIssue::KIND_BLOCKER,
				'field_key'  => '',
				'action_key' => 'resolve_template_variables',
				'path'       => '',
			);
		}

		return array(
			'type'        => (string) $type,
			'entity_id'   => isset( $fields['@id'] ) && is_string( $fields['@id'] ) ? $fields['@id'] : '',
			'graph_index' => 0,
			'emitted'     => false,
			'withheld'    => true,
			'source'      => self::SOURCE,
			'source_id'   => (string) $source_id,
			'source_ref'  => (string) $source_ref,
			'reason'      => (string) $reason,
			'diagnostics' => $diagnostics,
		);
	}

	/**
	 * Map readiness improvements to collector rows.
	 *
	 * @param \SEOvaultSEO\Schema\Readiness\SchemaReadinessResult|null $readiness Result.
	 * @param string                                                    $type      Type.
	 * @param string                                                    $source_ref Ref.
	 * @return array<int,array>
	 */
	private static function improvement_rows( $readiness, $type, $source_ref ) {
		if ( ! $readiness ) {
			return array();
		}
		$rows = array();
		foreach ( $readiness->get_improvements() as $issue ) {
			$row               = $issue->to_array();
			$row['type']       = (string) $type;
			$row['source']     = self::SOURCE;
			$row['source_ref'] = (string) $source_ref;
			$rows[]            = $row;
		}
		return $rows;
	}
}
