<?php
/**
 * Template JSON contract validation model.
 *
 * @package EntryFoundry
 */

namespace EntryFoundry\Core\Models\Template;

use EntryFoundry\Core\Services\Extension\ElementTypeRegistry;

// Exception messages are validation diagnostics, not rendered HTML output.
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped

/**
 * Validates the free Lite template contract.
 */
final class TemplateValidator {
	/**
	 * Registered element types.
	 *
	 * @var ElementTypeRegistry|null
	 */
	private $element_types;

	/**
	 * Initialize the validator.
	 *
	 * @param ElementTypeRegistry|null $element_types Registered element types.
	 */
	public function __construct( ?ElementTypeRegistry $element_types = null ) {
		$this->element_types = $element_types;
	}
	/**
	 * Free element types supported before the extension registry exists.
	 *
	 * @var string[]
	 */
	private const ELEMENT_TYPES = array( 'text', 'field', 'image', 'shape', 'table' );

	/**
	 * Decode and validate template JSON.
	 *
	 * @param string $json Template JSON.
	 * @return array<string, mixed>
	 * @throws ValidationException When validation fails.
	 */
	public function decode( $json ) {
		$template = json_decode( $json, true );
		if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $template ) ) {
			throw new ValidationException( 'Template must contain valid JSON: ' . json_last_error_msg() );
		}
		return $this->validate( $template );
	}

	/**
	 * Validate a decoded template.
	 *
	 * @param array<string, mixed> $template Decoded template.
	 * @return array<string, mixed>
	 * @throws ValidationException When validation fails.
	 */
	public function validate( array $template ) {
		$this->require_string( $template, 'version', 'template' );
		$this->require_string( $template, 'name', 'template' );
		$this->require_string( $template, 'profile', 'template' );
		if ( isset( $template['access_mode'] ) && ! in_array( $template['access_mode'], array( 'private', 'capability', 'public-token' ), true ) ) {
			throw new ValidationException( 'template.access_mode must be private, capability, or public-token.' );
		}
		$this->require_array( $template, 'source', 'template' );
		$this->require_array( $template, 'page', 'template' );
		$this->require_array( $template, 'elements', 'template' );

		$page = $template['page'];
		$this->require_string( $page, 'size', 'page' );
		$this->require_string( $page, 'orientation', 'page' );
		$this->require_positive_number( $page, 'width_mm', 'page' );
		$this->require_positive_number( $page, 'height_mm', 'page' );
		if ( ! in_array( $page['orientation'], array( 'portrait', 'landscape' ), true ) ) {
			throw new ValidationException( 'page.orientation must be portrait or landscape.' );
		}

		$ids = array();
		foreach ( $template['elements'] as $index => $element ) {
			if ( ! is_array( $element ) ) {
				throw new ValidationException( "elements[{$index}] must be an object." );
			}
			$context = "elements[{$index}]";
			$this->require_string( $element, 'id', $context );
			$this->require_string( $element, 'type', $context );
			if ( isset( $ids[ $element['id'] ] ) ) {
				throw new ValidationException( "{$context}.id must be unique." );
			}
			$ids[ $element['id'] ] = true;
			$supported             = $this->element_types ? $this->element_types->has( $element['type'] ) : in_array( $element['type'], self::ELEMENT_TYPES, true );
			if ( ! $supported ) {
				throw new ValidationException( "{$context}.type is unsupported in Lite." );
			}
			foreach ( array( 'x_mm', 'y_mm', 'w_mm', 'h_mm' ) as $coordinate ) {
				$this->require_number( $element, $coordinate, $context );
			}
			if ( $element['w_mm'] <= 0 || $element['h_mm'] <= 0 ) {
				throw new ValidationException( "{$context} width and height must be greater than zero." );
			}
			if ( 'field' === $element['type'] && ! isset( $element['field_id'] ) ) {
				throw new ValidationException( "{$context}.field_id is required for field elements." );
			}
			if ( 'image' === $element['type'] ) {
				$has_attachment = isset( $element['image_id'] ) && is_numeric( $element['image_id'] ) && (int) $element['image_id'] > 0;
				$has_source     = isset( $element['src'] ) && is_string( $element['src'] ) && '' !== trim( $element['src'] );
				if ( ! $has_attachment && ! $has_source ) {
					throw new ValidationException( "{$context} requires an image attachment or source." );
				}
			}
		}
		return $template;
	}

	/**
	 * Require an array value.
	 *
	 * @param array<string, mixed> $data Data.
	 * @param string               $key Key.
	 * @param string               $context Context.
	 * @return void
	 * @throws ValidationException When the key is absent or not an array.
	 */
	private function require_array( array $data, $key, $context ) {
		if ( ! isset( $data[ $key ] ) || ! is_array( $data[ $key ] ) ) {
			throw new ValidationException( "{$context}.{$key} must be an array or object." );
		}
	}

	/**
	 * Require a non-empty string value.
	 *
	 * @param array<string, mixed> $data Data.
	 * @param string               $key Key.
	 * @param string               $context Context.
	 * @return void
	 * @throws ValidationException When the key is not a non-empty string.
	 */
	private function require_string( array $data, $key, $context ) {
		if ( ! isset( $data[ $key ] ) || ! is_string( $data[ $key ] ) || '' === trim( $data[ $key ] ) ) {
			throw new ValidationException( "{$context}.{$key} must be a non-empty string." );
		}
	}

	/**
	 * Require a numeric value.
	 *
	 * @param array<string, mixed> $data Data.
	 * @param string               $key Key.
	 * @param string               $context Context.
	 * @return void
	 * @throws ValidationException When the key is not numeric.
	 */
	private function require_number( array $data, $key, $context ) {
		if ( ! isset( $data[ $key ] ) || ! is_numeric( $data[ $key ] ) ) {
			throw new ValidationException( "{$context}.{$key} must be numeric." );
		}
	}

	/**
	 * Require a positive numeric value.
	 *
	 * @param array<string, mixed> $data Data.
	 * @param string               $key Key.
	 * @param string               $context Context.
	 * @return void
	 * @throws ValidationException When the key is not positive.
	 */
	private function require_positive_number( array $data, $key, $context ) {
		$this->require_number( $data, $key, $context );
		if ( $data[ $key ] <= 0 ) {
			throw new ValidationException( "{$context}.{$key} must be greater than zero." );
		}
	}
}
