<?php
/**
 * Runs the versioned, partial local Google-oriented checks.
 *
 * @package SEOvaultSEO\Schema\Validation\Rules\Google
 */

namespace SEOvaultSEO\Schema\Validation\Rules\Google;

use SEOvaultSEO\Schema\Validation\RuleInterface;
use SEOvaultSEO\Schema\Validation\SchemaValidationFeatures;
use SEOvaultSEO\Schema\Validation\Severity;
use SEOvaultSEO\Schema\Validation\ValidationContext;
use SEOvaultSEO\Schema\Validation\ValidationIssue;
use SEOvaultSEO\Schema\Validation\ValueInspector;

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

/**
 * Evaluates conservative, documented property checks without remote requests.
 */
class GoogleRichResultRule implements RuleInterface {

	/**
	 * Get the stable rule code.
	 *
	 * @return string
	 */
	public function get_code() {
		return 'google.rich_result';
	}

	/**
	 * Google-oriented checks apply when a configured entity type is present.
	 *
	 * @param ValidationContext $context Validation context.
	 * @return bool
	 */
	public function supports( ValidationContext $context ) {
		if ( ! SchemaValidationFeatures::is_enabled( SchemaValidationFeatures::GOOGLE ) ) {
			return false;
		}

		$rules = $this->get_rule_data();
		foreach ( $context->get_entities() as $entity ) {
			if ( array_intersect( $entity['types'] ?? array(), array_keys( $rules['rules'] ) ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Evaluate all configured local property checks.
	 *
	 * @param ValidationContext $context Validation context.
	 * @return ValidationIssue[]
	 */
	public function evaluate( ValidationContext $context ) {
		$rule_data = $this->get_rule_data();
		$issues    = array();
		$evaluated = array();

		foreach ( $context->get_entities() as $entity ) {
			foreach ( $entity['types'] ?? array() as $type ) {
				if ( ! isset( $rule_data['rules'][ $type ] ) ) {
					continue;
				}

				$key = ( $entity['path'] ?? '' ) . '|' . $type;
				if ( isset( $evaluated[ $key ] ) ) {
					continue;
				}
				$evaluated[ $key ] = true;

				$issues = array_merge( $issues, $this->evaluate_rule( $entity, $type, $rule_data['rules'][ $type ], $context ) );
			}
		}

		return $issues;
	}

	/**
	 * Evaluate one entity against its partial rule pack.
	 *
	 * @param array             $entity  Normalized entity.
	 * @param string            $type    Matched entity type.
	 * @param array             $rule    Rule configuration.
	 * @param ValidationContext $context Validation context.
	 * @return ValidationIssue[]
	 */
	private function evaluate_rule( $entity, $type, $rule, ValidationContext $context ) {
		$issues = array();
		foreach ( $rule['required'] ?? array() as $property ) {
			if ( ! $this->has_value( $entity['data'] ?? array(), $property ) ) {
				$issues[] = $this->create_issue( $type, $property, Severity::ERROR, $entity, $context );
			}
		}

		foreach ( $rule['recommended'] ?? array() as $property ) {
			if ( ! $this->has_value( $entity['data'] ?? array(), $property ) ) {
				$issues[] = $this->create_issue( $type, $property, Severity::RECOMMENDATION, $entity, $context );
			}
		}

		foreach ( $rule['any_of'] ?? array() as $requirement ) {
			$found = false;
			foreach ( $requirement['properties'] ?? array() as $property ) {
				if ( $this->has_value( $entity['data'] ?? array(), $property ) ) {
					$found = true;
					break;
				}
			}
			if ( ! $found ) {
				$issues[] = $this->create_issue(
					$type,
					$requirement['code'] ?? 'required_property',
					isset( $requirement['severity'] ) && Severity::is_valid( $requirement['severity'] ) ? $requirement['severity'] : Severity::WARNING,
					$entity,
					$context
				);
			}
		}

		return $issues;
	}

	/**
	 * Determine whether a dot-separated property path has a non-empty value.
	 *
	 * @param array  $data Property data.
	 * @param string $path Dot-separated property path.
	 * @return bool
	 */
	private function has_value( $data, $path ) {
		$parts = explode( '.', $path );
		return $this->has_path_value( $data, $parts );
	}

	/**
	 * Recursively follow a property path through object or list values.
	 *
	 * @param mixed    $value Current value.
	 * @param string[] $parts Remaining property names.
	 * @return bool
	 */
	private function has_path_value( $value, $parts ) {
		if ( ! is_array( $value ) ) {
			return false;
		}

		if ( ValueInspector::is_list( $value ) ) {
			foreach ( $value as $item ) {
				if ( $this->has_path_value( $item, $parts ) ) {
					return true;
				}
			}
			return false;
		}

		$property = array_shift( $parts );
		if ( ! isset( $value[ $property ] ) && ! array_key_exists( $property, $value ) ) {
			return false;
		}
		if ( empty( $parts ) ) {
			return ! ValueInspector::is_empty( $value[ $property ] );
		}

		return $this->has_path_value( $value[ $property ], $parts );
	}

	/**
	 * Create a stable partial Google-rule issue.
	 *
	 * @param string            $type     Matched entity type.
	 * @param string            $property Missing property or group code.
	 * @param string            $severity Issue severity.
	 * @param array             $entity   Normalized entity.
	 * @param ValidationContext $context  Validation context.
	 * @return ValidationIssue
	 */
	private function create_issue( $type, $property, $severity, $entity, ValidationContext $context ) {
		$type_key     = strtolower( preg_replace( '/(?<!^)[A-Z]/', '_$0', $type ) );
		$property_key = strtolower( str_replace( '.', '_', preg_replace( '/(?<!^)[A-Z]/', '_$0', $property ) ) );
		$code         = 'google.' . $type_key . '.' . $property_key . '.missing';

		return new ValidationIssue(
			array(
				'code'          => $code,
				'severity'      => $severity,
				'category'      => 'google',
				'entity_type'   => $type,
				'entity_id'     => $entity['id'] ?? '',
				'path'          => ( $entity['path'] ?? '' ) . '.' . $property,
				'source'        => $context->get_source(),
				'fixable'       => true,
				'fix_action'    => 'add_schema_property',
				'documentation' => $type_key,
				'title_key'     => $code . '.title',
				'message_key'   => $code . '.message',
			)
		);
	}

	/**
	 * Load the local Google-oriented rule configuration.
	 *
	 * @return array
	 */
	private function get_rule_data() {
		$data = require dirname( __DIR__, 2 ) . '/Data/GoogleRichResultRules.php';
		if ( ! is_array( $data ) ) {
			$data = array(
				'version'    => '',
				'checked_at' => '',
				'rules'      => array(),
			);
		}

		/**
		 * Filters the bundled Google-oriented Schema Health rule pack.
		 *
		 * Invalid shapes are rejected; only array type entries with array
		 * property lists are retained. This filter must not bypass permission
		 * or nonce checks.
		 *
		 * @param array $data Rule pack with version, checked_at, and rules.
		 * @return array
		 */
		$filtered = apply_filters( 'seovaultseo_schema_validation_google_rules', $data );

		return $this->sanitize_rule_data( $filtered, $data );
	}

	/**
	 * Keep a safe Google rule pack shape.
	 *
	 * @param mixed $filtered Filtered value.
	 * @param array $fallback Default pack when the filter returns junk.
	 * @return array
	 */
	private function sanitize_rule_data( $filtered, $fallback ) {
		if ( ! is_array( $filtered ) || ! isset( $filtered['rules'] ) || ! is_array( $filtered['rules'] ) ) {
			return is_array( $fallback ) ? $fallback : array(
				'version'    => '',
				'checked_at' => '',
				'rules'      => array(),
			);
		}

		$rules = array();
		foreach ( $filtered['rules'] as $type => $rule ) {
			if ( ! is_string( $type ) || '' === $type || ! is_array( $rule ) ) {
				continue;
			}

			$entry = array();
			if ( isset( $rule['source_url'] ) && is_string( $rule['source_url'] ) ) {
				$entry['source_url'] = $rule['source_url'];
			}
			if ( isset( $rule['schema_org_url'] ) && is_string( $rule['schema_org_url'] ) ) {
				$entry['schema_org_url'] = $rule['schema_org_url'];
			}
			if ( isset( $rule['completeness'] ) && is_string( $rule['completeness'] ) ) {
				$entry['completeness'] = $rule['completeness'];
			}
			if ( isset( $rule['google_support_status'] ) && is_string( $rule['google_support_status'] ) ) {
				$entry['google_support_status'] = sanitize_key( $rule['google_support_status'] );
			}
			foreach ( array( 'required', 'recommended' ) as $list_key ) {
				if ( isset( $rule[ $list_key ] ) && is_array( $rule[ $list_key ] ) ) {
					$entry[ $list_key ] = array_values(
						array_filter(
							$rule[ $list_key ],
							function ( $property ) {
								return is_string( $property ) && '' !== $property;
							}
						)
					);
				}
			}
			if ( isset( $rule['any_of'] ) && is_array( $rule['any_of'] ) ) {
				$any_of = array();
				foreach ( $rule['any_of'] as $requirement ) {
					if ( ! is_array( $requirement ) || ! isset( $requirement['properties'] ) || ! is_array( $requirement['properties'] ) ) {
						continue;
					}
					$properties = array_values(
						array_filter(
							$requirement['properties'],
							function ( $property ) {
								return is_string( $property ) && '' !== $property;
							}
						)
					);
					if ( empty( $properties ) ) {
						continue;
					}
					$item = array( 'properties' => $properties );
					if ( isset( $requirement['code'] ) && is_string( $requirement['code'] ) ) {
						$item['code'] = $requirement['code'];
					}
					if ( isset( $requirement['severity'] ) && is_string( $requirement['severity'] ) ) {
						$item['severity'] = $requirement['severity'];
					}
					$any_of[] = $item;
				}
				if ( ! empty( $any_of ) ) {
					$entry['any_of'] = $any_of;
				}
			}

			$rules[ $type ] = $entry;
		}

		return array(
			'version'    => isset( $filtered['version'] ) && is_string( $filtered['version'] ) ? $filtered['version'] : ( $fallback['version'] ?? '' ),
			'checked_at' => isset( $filtered['checked_at'] ) && is_string( $filtered['checked_at'] ) ? $filtered['checked_at'] : ( $fallback['checked_at'] ?? '' ),
			'rules'      => $rules,
		);
	}
}
