<?php

/**
 * Deterministic Gutenberg block scaffolder (Route 1, no build step).
 *
 * @package LolaCore\BlockBuilder\Scaffold
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\BlockBuilder\Scaffold;

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

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

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

/**
 * Converts a structured spec into a validated block file map (block.json + assets).
 *
 * Same spec always yields the same bytes — snapshot-tested.
 */
final class BlockScaffolder {

	public const DEFAULT_CATEGORY = 'widgets';

	public const DEFAULT_ICON = 'info';

	private BlockJsonValidator $validator;

	public function __construct( ?BlockJsonValidator $validator = null ) {
		$this->validator = $validator ?? new BlockJsonValidator();
	}

	/**
	 * @param array<string, mixed> $spec
	 *
	 * @throws \InvalidArgumentException When the spec is invalid.
	 */
	public function scaffold( array $spec ): FilesMap {
		$normalized = $this->normalizeSpec($spec);
		$block_name = BlockPostType::blockNameFromSlug($normalized['slug']);

		$block_json = $this->buildBlockJson($normalized, $block_name);
		$edit_js    = $this->buildEditJs($normalized, $block_name);
		$style_css  = $this->buildStyleCss($normalized);

		$files = array(
			'block.json'     => wp_json_encode($block_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n",
			'edit.js'        => $edit_js,
			'edit.asset.php' => $this->buildEditAssetPhp(),
			'style.css'      => $style_css,
		);

		if ($normalized['dynamic']) {
			$files['render.php'] = $this->buildRenderPhp($normalized);
		}

		$map = FilesMap::fromArray($files);
		$this->validator->validateFilesMap($map);

		return $map;
	}

	/**
	 * @param array<string, mixed> $spec
	 * @return array{
	 *     title: string,
	 *     slug: string,
	 *     description: string,
	 *     category: string,
	 *     icon: string,
	 *     dynamic: bool,
	 *     inner_blocks: bool,
	 *     attributes: array<string, array<string, mixed>>,
	 *     supports: array<string, mixed>
	 * }
	 */
	public function normalizeSpec( array $spec ): array {
		$title = isset($spec['title']) ? trim((string) $spec['title']) : '';
		if ($title === '') {
			throw new \InvalidArgumentException(__('title is required in the block spec.', 'lolacore'));
		}

		$slug = isset($spec['slug']) ? sanitize_title((string) $spec['slug']) : '';
		if ($slug === '' || ! preg_match('/^[a-z][a-z0-9-]*$/', $slug)) {
			throw new \InvalidArgumentException(__('slug is required and must start with a letter and contain only lowercase letters, digits, and hyphens.', 'lolacore'));
		}

		$description = isset($spec['description']) ? trim((string) $spec['description']) : $title;
		$category    = isset($spec['category']) ? sanitize_key((string) $spec['category']) : self::DEFAULT_CATEGORY;
		$icon        = isset($spec['icon']) ? sanitize_key((string) $spec['icon']) : self::DEFAULT_ICON;

		if ($category === '') {
			$category = self::DEFAULT_CATEGORY;
		}

		$dynamic      = ! empty($spec['dynamic']);
		$inner_blocks = ! empty($spec['inner_blocks']) || ! empty($spec['innerBlocks']);

		$attributes = $this->normalizeAttributes($spec['attributes'] ?? array());
		$supports   = $this->normalizeSupports($spec['supports'] ?? array());

		return array(
			'title'        => $title,
			'slug'         => $slug,
			'description'  => $description,
			'category'     => $category,
			'icon'         => $icon,
			'dynamic'      => $dynamic,
			'inner_blocks' => $inner_blocks,
			'attributes'   => $attributes,
			'supports'     => $supports,
		);
	}

	/**
	 * @param array<string, mixed> $normalized
	 * @return array<string, mixed>
	 */
	private function buildBlockJson( array $normalized, string $block_name ): array {
		$json = array(
			'$schema'    => 'https://schemas.wp.org/trunk/block.json',
			'apiVersion' => 3,
			'name'       => $block_name,
			'title'      => $normalized['title'],
			'category'   => $normalized['category'],
			'icon'       => $normalized['icon'],
			'description'=> $normalized['description'],
			'textdomain' => 'lolacore-block-' . $normalized['slug'],
			'attributes' => $normalized['attributes'],
			'supports'   => $normalized['supports'],
			'editorScript' => 'file:./edit.js',
			'style'        => 'file:./style.css',
		);

		if ($normalized['dynamic']) {
			$json['render'] = 'file:./render.php';
		}

		return $json;
	}

	/**
	 * @param array<mixed, mixed> $raw
	 * @return array<string, array<string, mixed>>
	 */
	private function normalizeAttributes( array $raw ): array {
		$attributes = array();
		foreach ($raw as $key => $definition) {
			if (! is_string($key) || ! is_array($definition)) {
				continue;
			}

			$name = sanitize_key($key);
			if ($name === '') {
				continue;
			}

			$type = isset($definition['type']) ? sanitize_key((string) $definition['type']) : 'string';
			if (! in_array($type, array('string', 'number', 'boolean', 'array', 'object'), true)) {
				$type = 'string';
			}

			$entry = array('type' => $type);
			if (array_key_exists('default', $definition)) {
				$entry['default'] = $definition['default'];
			}

			$attributes[ $name ] = $entry;
		}

		return $attributes;
	}

	/**
	 * @param array<mixed, mixed> $raw
	 * @return array<string, mixed>
	 */
	private function normalizeSupports( array $raw ): array {
		if ($raw === array()) {
			return array(
				'html' => false,
			);
		}

		return $raw;
	}

	/**
	 * @param array<string, mixed> $normalized
	 */
	private function buildEditJs( array $normalized, string $block_name ): string {
		$inner = $normalized['inner_blocks']
			? "\n\t\t\tel( InnerBlocks, null ),"
			: $this->buildAttributePreviewLines($normalized['attributes']);

		$save_fn = '';
		if (! $normalized['dynamic']) {
			$save_inner = $normalized['inner_blocks']
				? "\n\t\t\tel( InnerBlocks.Content, null ),"
				: $this->buildAttributeSaveLines($normalized['attributes']);

			$save_fn = "\n\n\t\tsave: function ( props ) {\n"
				. "\t\t\tvar blockProps = useBlockProps.save();\n"
				. "\t\t\treturn el(\n"
				. "\t\t\t\t'div',\n"
				. "\t\t\t\tblockProps,{$save_inner}\n"
				. "\t\t\t);\n"
				. "\t\t},";
		}

		$inner_blocks_import = $normalized['inner_blocks']
			? "\n\tvar InnerBlocks = wp.blockEditor.InnerBlocks;"
			: '';

		return "( function ( wp ) {\n"
			. "\tvar el = wp.element.createElement;\n"
			. "\tvar registerBlockType = wp.blocks.registerBlockType;\n"
			. "\tvar useBlockProps = wp.blockEditor.useBlockProps;{$inner_blocks_import}\n\n"
			. "\tregisterBlockType( '{$block_name}', {\n"
			. "\t\tedit: function ( props ) {\n"
			. "\t\t\tvar blockProps = useBlockProps();\n"
			. "\t\t\treturn el(\n"
			. "\t\t\t\t'div',\n"
			. "\t\t\t\tblockProps,{$inner}\n"
			. "\t\t\t);\n"
			. "\t\t},{$save_fn}\n"
			. "\t} );\n"
			. "} )( window.wp );\n";
	}

	/**
	 * @param array<string, array<string, mixed>> $attributes
	 */
	private function buildAttributePreviewLines( array $attributes ): string {
		if ($attributes === array()) {
			return "\n\t\t\tprops.attributes.content || ''";
		}

		$lines = array();
		foreach (array_keys($attributes) as $name) {
			$lines[] = "\n\t\t\tprops.attributes.{$name} || ''";
		}

		return implode( ',', $lines );
	}

	/**
	 * @param array<string, array<string, mixed>> $attributes
	 */
	private function buildAttributeSaveLines( array $attributes ): string {
		if ($attributes === array()) {
			return "\n\t\t\tprops.attributes.content || ''";
		}

		$lines = array();
		foreach (array_keys($attributes) as $name) {
			$lines[] = "\n\t\t\tprops.attributes.{$name} || ''";
		}

		return implode( ',', $lines );
	}

	/**
	 * WordPress loads editorScript dependencies from the sibling *.asset.php file.
	 * Without it, edit.js is enqueued with no wp-blocks / wp-block-editor deps and
	 * registerBlockType never runs in the editor.
	 */
	private function buildEditAssetPhp(): string {
		return <<<'PHP'
<?php
return array(
	'dependencies' => array(
		'wp-blocks',
		'wp-element',
		'wp-block-editor',
	),
	'version'      => '1.0.0',
);

PHP;
	}

	/**
	 * @param array<string, mixed> $normalized
	 */
	private function buildStyleCss( array $normalized ): string {
		$class = 'wp-block-' . str_replace('/', '-', BlockPostType::blockNameFromSlug($normalized['slug']));

		return ".{$class} {\n\tdisplay: block;\n}\n";
	}

	/**
	 * @param array<string, mixed> $normalized
	 */
	private function buildRenderPhp( array $normalized ): string {
		$block_name = BlockPostType::blockNameFromSlug($normalized['slug']);
		$attrs      = array_keys($normalized['attributes']);
		$output     = $attrs !== array() ? '$attributes[\'' . $attrs[0] . '\'] ?? \'\'' : '\'\'';

		return '<?php' . "\n"
			. '/**' . "\n"
			. ' * Server-side render for ' . $block_name . '.' . "\n"
			. ' *' . "\n"
			. ' * @package LolaCore\BlockBuilder' . "\n"
			. ' */' . "\n\n"
			. 'if ( ! defined( \'ABSPATH\' ) ) {' . "\n"
			. "\texit;\n"
			. '}' . "\n\n"
			. "\$content = {$output};\n"
			. '?>' . "\n"
			. '<div <?php echo get_block_wrapper_attributes(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>' . "\n"
			. "\t<?php echo esc_html( (string) \$content ); ?>\n"
			. '</div>' . "\n";
	}
}

