<?php

/**
 * Validates block.json metadata and file maps for the Block Builder.
 *
 * @package LolaCore\BlockBuilder\Validation
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\BlockBuilder\Validation;

// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Diagnostic exception messages; escaped at REST/chat presentation layer.

use LolaCore\BlockBuilder\PostType\BlockPostType;
use LolaCore\Builder\Artifact\FilesMap;

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

/**
 * Enforces Route 1 constraints and reserved lolacore/* naming.
 */
final class BlockJsonValidator {

	/** @var array<int, int> */
	public const SUPPORTED_API_VERSIONS = array( 2, 3 );

	private const BLOCK_NAME_PATTERN = '/^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/';

	/**
	 * Validate block.json content and referenced files in the map.
	 *
	 * @throws \InvalidArgumentException When validation fails.
	 */
	public function validateFilesMap( FilesMap $files ): void {
		if (! $files->has('block.json')) {
			throw new \InvalidArgumentException(__('block.json is required in the file map.', 'lolacore'));
		}

		$decoded = json_decode($files->get('block.json'), true);
		if (! is_array($decoded)) {
			throw new \InvalidArgumentException(__('block.json must be valid JSON.', 'lolacore'));
		}

		$this->validateBlockJson($decoded, $files);
	}

	/**
	 * @param array<string, mixed> $block_json
	 *
	 * @throws \InvalidArgumentException When validation fails.
	 */
	public function validateBlockJson( array $block_json, FilesMap $files ): void {
		$name = isset($block_json['name']) ? (string) $block_json['name'] : '';
		$this->validateBlockName($name);

		$api_version = isset($block_json['apiVersion']) ? (int) $block_json['apiVersion'] : 0;
		if (! in_array($api_version, self::SUPPORTED_API_VERSIONS, true)) {
			throw new \InvalidArgumentException(
				sprintf(
					/* translators: %d: unsupported apiVersion value */
					__('apiVersion %d is not supported. Use 2 or 3.', 'lolacore'),
					$api_version
				)
			);
		}

		if (isset($block_json['viewScriptModule'])) {
			throw new \InvalidArgumentException(__('viewScriptModule is not supported in Route 1 blocks.', 'lolacore'));
		}

		$this->assertReferencedFilesExist($block_json, $files);
	}

	/**
	 * @throws \InvalidArgumentException When the name is invalid.
	 */
	public function validateBlockName( string $name ): void {
		if ($name === '') {
			throw new \InvalidArgumentException(__('block.json name is required.', 'lolacore'));
		}

		if (! preg_match(self::BLOCK_NAME_PATTERN, $name)) {
			throw new \InvalidArgumentException(__('block.json name must match namespace/slug (lowercase letters, digits, hyphens).', 'lolacore'));
		}

		$prefix = BlockPostType::BLOCK_NAMESPACE . '/';
		if (! str_starts_with($name, $prefix)) {
			throw new \InvalidArgumentException(
				sprintf(
					/* translators: %s: reserved block namespace */
					__('block.json name must use the reserved namespace "%s".', 'lolacore'),
					BlockPostType::BLOCK_NAMESPACE
				)
			);
		}
	}

	/**
	 * Reject install when the block type is already registered on this site.
	 *
	 * @throws \InvalidArgumentException When the block name collides.
	 */
	public function assertBlockNameAvailable( string $name ): void {
		$this->validateBlockName($name);

		if (! class_exists(\WP_Block_Type_Registry::class)) {
			return;
		}

		$registry = \WP_Block_Type_Registry::get_instance();
		if ($registry->is_registered($name)) {
			throw new \InvalidArgumentException(
				sprintf(
					/* translators: %s: block name */
					__('Block "%s" is already registered on this site.', 'lolacore'),
					$name
				)
			);
		}
	}

	/**
	 * @param array<string, mixed> $block_json
	 */
	private function assertReferencedFilesExist( array $block_json, FilesMap $files ): void {
		$file_refs = array(
			'editorScript',
			'editorStyle',
			'style',
			'script',
			'viewScript',
			'render',
		);

		foreach ($file_refs as $key) {
			if (! isset($block_json[ $key ])) {
				continue;
			}

			$value = $block_json[ $key ];
			if (! is_string($value)) {
				continue;
			}

			$rel = $this->resolveFileReference($value);
			if ($rel === null) {
				continue;
			}

			if (! $files->has($rel)) {
				throw new \InvalidArgumentException(
					sprintf(
						/* translators: 1: relative file path, 2: block.json property key */
						__('block.json references missing file "%1$s" (%2$s).', 'lolacore'),
						$rel,
						$key
					)
				);
			}
		}
	}

	/**
	 * Resolve "file:./edit.js" style references to a map-relative path.
	 */
	private function resolveFileReference( string $reference ): ?string {
		if (! str_starts_with($reference, 'file:')) {
			return null;
		}

		$path = substr($reference, 5);
		$path = ltrim($path, './');

		return $path !== '' ? $path : null;
	}
}

