<?php
/**
 * Shared value helpers for readiness policies.
 *
 * @package SEOvaultSEO\Schema\Readiness
 */

namespace SEOvaultSEO\Schema\Readiness;

use SEOvaultSEO\Schema\Validation\ValueInspector;

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

/**
 * Non-mutating helpers for readiness checks.
 */
class SchemaReadinessValues {

	/**
	 * Max encoded payload size accepted for readiness evaluation.
	 *
	 * @var int
	 */
	const MAX_PAYLOAD_BYTES = 262144;

	/**
	 * Placeholder strings that never count as ready content.
	 *
	 * @var string[]
	 */
	private static $placeholders = array(
		'answer goes here',
		'question goes here',
		'insert answer',
		'insert question',
		'todo',
		'placeholder',
		'lorem ipsum',
		'n/a',
		'na',
		'tbd',
		'coming soon',
	);

	/**
	 * Whether a value is meaningful for readiness.
	 *
	 * Nested objects that contain only `@type` (and optionally empty noise) are empty.
	 *
	 * @param mixed $value Candidate value.
	 * @return bool
	 */
	public static function is_meaningful( $value ) {
		if ( null === $value || false === $value ) {
			return false;
		}

		if ( is_int( $value ) || is_float( $value ) ) {
			return true;
		}

		if ( is_bool( $value ) ) {
			return true;
		}

		if ( is_string( $value ) ) {
			$trimmed = trim( $value );
			if ( '' === $trimmed ) {
				return false;
			}
			if ( self::is_placeholder( $trimmed ) ) {
				return false;
			}
			if ( preg_match( '/^%[a-z0-9_.]+%$/i', $trimmed ) ) {
				return false;
			}
			return true;
		}

		if ( ! is_array( $value ) ) {
			return false;
		}

		if ( array() === $value ) {
			return false;
		}

		if ( ValueInspector::is_list( $value ) ) {
			foreach ( $value as $item ) {
				if ( self::is_meaningful( $item ) ) {
					return true;
				}
			}
			return false;
		}

		$has_meaningful = false;
		foreach ( $value as $key => $item ) {
			$key = (string) $key;
			if ( '@type' === $key ) {
				continue;
			}
			if ( self::is_meaningful( $item ) ) {
				$has_meaningful = true;
				break;
			}
		}

		return $has_meaningful;
	}

	/**
	 * Whether text looks like a known placeholder.
	 *
	 * @param string $value Text.
	 * @return bool
	 */
	public static function is_placeholder( $value ) {
		$normalized = strtolower( trim( (string) $value ) );
		$normalized = preg_replace( '/\s+/', ' ', $normalized );
		return in_array( $normalized, self::$placeholders, true );
	}

	/**
	 * Read a field that may live at top level or under nested path.
	 *
	 * @param array  $fields Fields/entity.
	 * @param string $key    Field key.
	 * @return mixed|null
	 */
	public static function get( array $fields, $key ) {
		if ( array_key_exists( $key, $fields ) ) {
			return $fields[ $key ];
		}
		return null;
	}

	/**
	 * First non-empty among alternate keys.
	 *
	 * @param array    $fields Fields.
	 * @param string[] $keys   Keys.
	 * @return mixed|null
	 */
	public static function first_meaningful( array $fields, array $keys ) {
		foreach ( $keys as $key ) {
			$value = self::get( $fields, $key );
			if ( self::is_meaningful( $value ) ) {
				return $value;
			}
		}
		return null;
	}

	/**
	 * Whether encoded payload exceeds size budget.
	 *
	 * @param array $fields Fields.
	 * @return bool
	 */
	public static function is_oversized( array $fields ) {
		$encoded = wp_json_encode( $fields );
		if ( ! is_string( $encoded ) ) {
			return true;
		}
		return strlen( $encoded ) > self::MAX_PAYLOAD_BYTES;
	}

	/**
	 * Validate a date/datetime string.
	 *
	 * @param mixed $value Candidate.
	 * @return bool
	 */
	public static function is_valid_date( $value ) {
		return is_string( $value ) && ValueInspector::is_valid_date( trim( $value ) );
	}

	/**
	 * Parse a date string to UNIX timestamp (UTC midnight for date-only).
	 *
	 * @param string $value Date string.
	 * @return int|false
	 */
	public static function to_timestamp( $value ) {
		if ( ! self::is_valid_date( $value ) ) {
			return false;
		}
		$value = trim( (string) $value );
		if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ) {
			$dt = \DateTime::createFromFormat( '!Y-m-d', $value, new \DateTimeZone( 'UTC' ) );
			return $dt ? $dt->getTimestamp() : false;
		}
		$dt = date_create( $value );
		return $dt ? $dt->getTimestamp() : false;
	}

	/**
	 * Whether a price value is acceptable (including free "0").
	 *
	 * @param mixed $value Price.
	 * @return bool
	 */
	public static function is_valid_price( $value ) {
		if ( is_int( $value ) || is_float( $value ) ) {
			return $value >= 0;
		}
		if ( ! is_string( $value ) ) {
			return false;
		}
		$trimmed = trim( $value );
		if ( '' === $trimmed || self::is_placeholder( $trimmed ) ) {
			return false;
		}
		if ( ! preg_match( '/^-?\d+(\.\d+)?$/', $trimmed ) ) {
			return false;
		}
		return (float) $trimmed >= 0;
	}

	/**
	 * Whether a rating is within inclusive bounds.
	 *
	 * @param mixed $value Rating.
	 * @param float $min   Min.
	 * @param float $max   Max.
	 * @return bool
	 */
	public static function is_rating_in_bounds( $value, $min = 1.0, $max = 5.0 ) {
		if ( is_string( $value ) ) {
			$value = trim( $value );
			if ( '' === $value || ! is_numeric( $value ) ) {
				return false;
			}
			$value = (float) $value;
		}
		if ( ! is_int( $value ) && ! is_float( $value ) ) {
			return false;
		}
		return $value >= $min && $value <= $max;
	}

	/**
	 * Extract offer price from offers object/array.
	 *
	 * @param mixed $offers Offers node.
	 * @return mixed|null
	 */
	public static function extract_offer_price( $offers ) {
		if ( ! self::is_meaningful( $offers ) ) {
			return null;
		}
		if ( ValueInspector::is_list( $offers ) ) {
			foreach ( $offers as $offer ) {
				$price = self::extract_offer_price( $offer );
				if ( null !== $price ) {
					return $price;
				}
			}
			return null;
		}
		if ( is_array( $offers ) && array_key_exists( 'price', $offers ) ) {
			return $offers['price'];
		}
		return null;
	}

	/**
	 * Normalize a list field to an array of items.
	 *
	 * @param mixed $value List or single item.
	 * @return array
	 */
	public static function as_list( $value ) {
		if ( null === $value ) {
			return array();
		}
		if ( is_array( $value ) && ValueInspector::is_list( $value ) ) {
			return $value;
		}
		return array( $value );
	}
}
