<?php
/**
 * Server-authoritative Quick Answer block attribute sanitizer.
 *
 * Visible AEO/reader content only — never invents Schema.org entities.
 *
 * @package SEOvaultSEO\Blocks
 */

namespace SEOvaultSEO\Blocks;

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

/**
 * Sanitizes and normalizes seovault/quick-answer block attributes.
 */
class QuickAnswerBlockAttributeSanitizer {

	/**
	 * Supported schemaVersion values (block contract, not Schema.org).
	 */
	const SCHEMA_VERSION = 1;

	/**
	 * Default attributes for a new block.
	 *
	 * @return array<string,mixed>
	 */
	public static function defaults() {
		return array(
			'schemaVersion' => self::SCHEMA_VERSION,
			'label'         => 'Quick answer',
			'answer'        => '',
			'source'        => '',
			'stylePreset'   => 'clean',
			'colorPreset'   => 'blue',
			'density'       => 'comfortable',
		);
	}

	/**
	 * Attribute schema for register_block_type.
	 *
	 * @return array<string,array<string,mixed>>
	 */
	public static function attribute_schema() {
		return array(
			'schemaVersion' => array(
				'type'    => 'number',
				'default' => self::SCHEMA_VERSION,
			),
			'label'         => array(
				'type'    => 'string',
				'default' => 'Quick answer',
			),
			'answer'        => array(
				'type'    => 'string',
				'default' => '',
			),
			'source'        => array(
				'type'    => 'string',
				'default' => '',
			),
			'stylePreset'   => array(
				'type'    => 'string',
				'default' => 'clean',
			),
			'colorPreset'   => array(
				'type'    => 'string',
				'default' => 'blue',
			),
			'density'       => array(
				'type'    => 'string',
				'default' => 'comfortable',
			),
		);
	}

	/**
	 * Sanitize a raw attributes bag.
	 *
	 * @param mixed $raw     Raw attributes.
	 * @param array $context Optional limit overrides / flags.
	 * @return array{attributes:array<string,mixed>,errors:string[],stripped:string[]}
	 */
	public static function sanitize( $raw, array $context = array() ) {
		$defaults = self::defaults();
		$errors   = array();
		$stripped = array();
		$limits   = QuickAnswerBlockLimits::all( $context );

		if ( ! is_array( $raw ) ) {
			$errors[] = 'attributes_not_array';
			return array(
				'attributes' => $defaults,
				'errors'     => $errors,
				'stripped'   => $stripped,
			);
		}

		$encoded = wp_json_encode( $raw );
		if ( is_string( $encoded ) && strlen( $encoded ) > $limits['max_payload_bytes'] ) {
			$errors[] = 'payload_too_large';
			return array(
				'attributes' => $defaults,
				'errors'     => $errors,
				'stripped'   => $stripped,
			);
		}

		$allowed_keys = array_keys( $defaults );
		foreach ( array_keys( $raw ) as $key ) {
			if ( ! in_array( (string) $key, $allowed_keys, true ) ) {
				$stripped[] = (string) $key;
			}
		}

		$out = $defaults;

		$version = isset( $raw['schemaVersion'] ) ? (int) $raw['schemaVersion'] : self::SCHEMA_VERSION;
		if ( self::SCHEMA_VERSION !== $version ) {
			$errors[] = 'unsupported_schema_version';
			$version  = self::SCHEMA_VERSION;
		}
		$out['schemaVersion'] = $version;

		$label = self::sanitize_plain_text(
			isset( $raw['label'] ) ? $raw['label'] : 'Quick answer',
			$limits['max_label_length']
		);
		$out['label'] = '' !== $label ? $label : 'Quick answer';

		$out['answer'] = self::sanitize_plain_text(
			isset( $raw['answer'] ) ? $raw['answer'] : ( isset( $raw['tldr'] ) ? $raw['tldr'] : '' ),
			$limits['max_answer_length']
		);

		$source_result = self::sanitize_source(
			isset( $raw['source'] ) ? $raw['source'] : '',
			$limits['max_source_length']
		);
		$out['source'] = $source_result['value'];
		if ( ! empty( $source_result['error'] ) ) {
			$errors[] = $source_result['error'];
		}

		$out['stylePreset'] = self::sanitize_enum(
			isset( $raw['stylePreset'] ) ? $raw['stylePreset'] : '',
			QuickAnswerBlockLimits::style_presets(),
			'clean',
			$errors,
			'stylePreset'
		);
		$out['colorPreset'] = self::sanitize_enum(
			isset( $raw['colorPreset'] ) ? $raw['colorPreset'] : '',
			QuickAnswerBlockLimits::color_presets(),
			'blue',
			$errors,
			'colorPreset'
		);
		$out['density'] = self::sanitize_enum(
			isset( $raw['density'] ) ? $raw['density'] : '',
			QuickAnswerBlockLimits::densities(),
			'comfortable',
			$errors,
			'density'
		);

		return array(
			'attributes' => $out,
			'errors'     => array_values( array_unique( $errors ) ),
			'stripped'   => array_values( array_unique( $stripped ) ),
		);
	}

	/**
	 * Whether the sanitized block has enough visible content to display.
	 *
	 * @param array $attributes Sanitized attributes.
	 * @return bool
	 */
	public static function has_visible_content( array $attributes ) {
		$answer = isset( $attributes['answer'] ) ? trim( (string) $attributes['answer'] ) : '';
		return '' !== $answer;
	}

	/**
	 * Sanitize diagnostic source token.
	 *
	 * @param mixed $value Raw.
	 * @param int   $max   Max length.
	 * @return array{value:string,error:?string}
	 */
	private static function sanitize_source( $value, $max ) {
		$text = self::sanitize_plain_text( $value, $max );
		if ( '' === $text ) {
			return array(
				'value' => '',
				'error' => null,
			);
		}

		$text = strtolower( $text );
		if ( ! preg_match( '/^[a-z][a-z0-9_-]{0,39}$/', $text ) ) {
			return array(
				'value' => '',
				'error' => 'invalid_source',
			);
		}

		$base = explode( '-', $text, 2 );
		$base = isset( $base[0] ) ? $base[0] : '';
		if ( ! in_array( $base, QuickAnswerBlockLimits::source_bases(), true ) ) {
			return array(
				'value' => '',
				'error' => 'invalid_source',
			);
		}

		return array(
			'value' => $text,
			'error' => null,
		);
	}

	/**
	 * Strip tags and truncate plain text.
	 *
	 * @param mixed $value Raw.
	 * @param int   $max   Max length.
	 * @return string
	 */
	private static function sanitize_plain_text( $value, $max ) {
		if ( is_array( $value ) || is_object( $value ) ) {
			return '';
		}
		$value = self::decode_block_attr_string( (string) $value );
		$text  = wp_strip_all_tags( (string) $value );
		$text  = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		$text  = preg_replace( '/[\x00-\x1F\x7F]+/u', ' ', is_string( $text ) ? $text : '' );
		$text  = preg_replace( '/\s+/u', ' ', is_string( $text ) ? $text : '' );
		$text  = is_string( $text ) ? trim( $text ) : '';
		if ( function_exists( 'mb_substr' ) ) {
			return mb_substr( $text, 0, (int) $max );
		}
		return substr( $text, 0, (int) $max );
	}

	/**
	 * Allowlist enum with fallback.
	 *
	 * @param mixed    $value    Raw.
	 * @param string[] $allowed  Allowed values.
	 * @param string   $fallback Fallback.
	 * @param string[] $errors   Error bag (by ref).
	 * @param string   $key      Attribute key.
	 * @return string
	 */
	private static function sanitize_enum( $value, array $allowed, $fallback, array &$errors, $key ) {
		$candidate = is_string( $value ) || is_numeric( $value ) ? (string) $value : '';
		if ( in_array( $candidate, $allowed, true ) ) {
			return $candidate;
		}
		if ( '' !== $candidate ) {
			$errors[] = 'invalid_enum_' . $key;
		}
		return $fallback;
	}

	/**
	 * Decode common entity-encoded attribute strings from block comments.
	 *
	 * @param string $value Raw.
	 * @return string
	 */
	public static function decode_block_attr_string( $value ) {
		$value = (string) $value;
		if ( false === strpos( $value, '&' ) ) {
			return $value;
		}
		return html_entity_decode( $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
	}
}
