<?php
/**
 * Trigger Context Value Object
 *
 * Immutable value object representing the runtime context data
 * that flows through the execution pipeline (trigger -> queue -> executor).
 *
 * @package Sequensy
 * @since 2.0.0
 */

declare(strict_types=1);

namespace Sequensy\ValueObjects;

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Class TriggerContext
 *
 * Provides typed, immutable access to trigger execution context data.
 * Replaces raw associative arrays flowing through the execution pipeline.
 */
final class TriggerContext implements \JsonSerializable {

	/**
	 * Context data.
	 *
	 * @var array<string, mixed>
	 */
	private readonly array $data;

	/**
	 * Private constructor - use from_array() factory.
	 *
	 * @param array<string, mixed> $data Context data.
	 */
	private function __construct( array $data ) {
		$this->data = $data;
	}

	/**
	 * Create a TriggerContext from an associative array.
	 *
	 * @param array<string, mixed> $data Context data.
	 * @return self New TriggerContext instance.
	 *
	 * @throws \InvalidArgumentException If data is not JSON-serializable.
	 */
	public static function from_array( array $data ): self {
		$json = wp_json_encode( $data );
		if ( false === $json ) {
			throw new \InvalidArgumentException(
				'Context data must be JSON-serializable: ' . esc_html( json_last_error_msg() )
			);
		}

		return new self( $data );
	}

	/**
	 * Get the trigger type identifier.
	 *
	 * @return string Trigger type, or empty string if not set.
	 */
	public function get_trigger_type(): string {
		return (string) ( $this->data['trigger_type'] ?? '' );
	}

	/**
	 * Get a value by key, supporting dot notation for nested access.
	 *
	 * @param string $key           Data key (supports dot notation, e.g. 'user.meta.role').
	 * @param mixed  $default_value Default value if key not found.
	 * @return mixed Value or default.
	 */
	public function get( string $key, mixed $default_value = null ): mixed {
		if ( array_key_exists( $key, $this->data ) ) {
			return $this->data[ $key ];
		}

		if ( str_contains( $key, '.' ) ) {
			return $this->get_nested( $key, $default_value );
		}

		return $default_value;
	}

	/**
	 * Return a new TriggerContext with additional data merged in.
	 *
	 * The original instance is not modified (immutable).
	 *
	 * @param array<string, mixed> $additional Data to merge.
	 * @return self New TriggerContext with merged data.
	 */
	public function merge( array $additional ): self {
		return self::from_array( array_merge( $this->data, $additional ) );
	}

	/**
	 * Convert to array.
	 *
	 * @return array<string, mixed> All context data.
	 */
	public function to_array(): array {
		return $this->data;
	}

	/**
	 * Implement JsonSerializable for REST API responses and serialization.
	 *
	 * @return array<string, mixed> Data to serialize.
	 */
	public function jsonSerialize(): array {
		return $this->data;
	}

	/**
	 * Resolve a dot-notation key against nested arrays.
	 *
	 * @param string $key           Dot-separated key path.
	 * @param mixed  $default_value Default value if path not found.
	 * @return mixed Resolved value or default.
	 */
	private function get_nested( string $key, mixed $default_value ): mixed {
		$keys  = explode( '.', $key );
		$value = $this->data;

		foreach ( $keys as $segment ) {
			if ( ! is_array( $value ) || ! array_key_exists( $segment, $value ) ) {
				return $default_value;
			}
			$value = $value[ $segment ];
		}

		return $value;
	}
}
