<?php
/**
 * Visual element node — single source of truth unit.
 *
 * @package AuraBuilder
 */

declare(strict_types=1);

namespace AuraBuilder\Document;

final class Node {

	public string $id;
	public string $type;
	public ?string $parent_id;

	/** @var list<string> */
	public array $children;

	/** @var array<string, mixed> */
	public array $settings;

	/** @var array{desktop?:array<string,mixed>,tablet?:array<string,mixed>,mobile?:array<string,mixed>} */
	public array $styles;

	/** @var array<string, string> */
	public array $attributes;

	/** @var list<string> */
	public array $classes;

	public bool $locked;
	public bool $hidden;

	/**
	 * @param array<string, mixed> $data
	 */
	public function __construct( array $data ) {
		$this->id         = (string) ( $data['id'] ?? self::generate_id() );
		$this->type       = sanitize_key( (string) ( $data['type'] ?? 'container' ) );
		$this->parent_id  = isset( $data['parentId'] ) ? (string) $data['parentId'] : ( isset( $data['parent_id'] ) ? (string) $data['parent_id'] : null );
		$this->children   = self::string_list( $data['children'] ?? array() );
		$this->settings   = is_array( $data['settings'] ?? null ) ? $data['settings'] : array();
		$this->styles     = self::styles( $data['styles'] ?? array() );
		$this->attributes = self::string_map( $data['attributes'] ?? array() );
		$this->classes    = self::string_list( $data['classes'] ?? array() );
		$this->locked     = ! empty( $data['locked'] );
		$this->hidden     = ! empty( $data['hidden'] );
	}

	public static function generate_id(): string {
		return 'node_' . wp_generate_password( 10, false, false );
	}

	/**
	 * @return array<string, mixed>
	 */
	public function to_array(): array {
		// Force {} not [] in JSON for empty style bags (JS loses keys on arrays).
		$styles = array(
			'desktop' => empty( $this->styles['desktop'] ) ? new \stdClass() : $this->styles['desktop'],
			'tablet'  => empty( $this->styles['tablet'] ) ? new \stdClass() : $this->styles['tablet'],
			'mobile'  => empty( $this->styles['mobile'] ) ? new \stdClass() : $this->styles['mobile'],
		);
		return array(
			'id'         => $this->id,
			'type'       => $this->type,
			'parentId'   => $this->parent_id,
			'children'   => $this->children,
			'settings'   => $this->settings,
			'styles'     => $styles,
			'attributes' => $this->attributes,
			'classes'    => $this->classes,
			'locked'     => $this->locked,
			'hidden'     => $this->hidden,
		);
	}

	/**
	 * @param mixed $v
	 * @return list<string>
	 */
	private static function string_list( mixed $v ): array {
		if ( ! is_array( $v ) ) {
			return array();
		}
		$out = array();
		foreach ( $v as $item ) {
			if ( is_string( $item ) && $item !== '' ) {
				$out[] = $item;
			}
		}
		return $out;
	}

	/**
	 * @param mixed $v
	 * @return array<string, string>
	 */
	private static function string_map( mixed $v ): array {
		if ( ! is_array( $v ) ) {
			return array();
		}
		$out = array();
		foreach ( $v as $k => $val ) {
			if ( is_string( $k ) && ( is_string( $val ) || is_numeric( $val ) ) ) {
				$out[ $k ] = (string) $val;
			}
		}
		return $out;
	}

	/**
	 * @param mixed $v
	 * @return array{desktop?:array<string,mixed>,tablet?:array<string,mixed>,mobile?:array<string,mixed>}
	 */
	private static function styles( mixed $v ): array {
		if ( is_object( $v ) ) {
			$v = (array) $v;
		}
		if ( ! is_array( $v ) ) {
			return array();
		}
		$out = array();
		foreach ( array( 'desktop', 'tablet', 'mobile' ) as $bp ) {
			if ( ! isset( $v[ $bp ] ) ) {
				continue;
			}
			$bag = $v[ $bp ];
			// WP post_meta may restore empty bags as stdClass from JSON/object storage.
			if ( is_object( $bag ) ) {
				$bag = (array) $bag;
			}
			if ( is_array( $bag ) ) {
				$out[ $bp ] = $bag;
			}
		}
		return $out;
	}
}
