<?php
/**
 * Provider object context.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Provider;

use InvalidArgumentException;

/**
 * Identifies an object without coupling provider contracts to WC data objects.
 */
final class ObjectContext {
	private string $targetType;
	private ?int $targetId  = null;
	private string $subtype = '';
	/**
	 * @var array<string, mixed>
	 */
	private array $attributes = array();
	/**
	 * @param array<string,mixed> $attributes Provider-independent context hints.
	 */
	public function __construct(
		string $targetType,
		?int $targetId = null,
		string $subtype = '',
		array $attributes = array()
	) {
		$this->targetType = $targetType;
		$this->targetId   = $targetId;
		$this->subtype    = $subtype;
		$this->attributes = $attributes;
		if ( '' === trim( $this->targetType ) ) {
			throw new InvalidArgumentException( 'Object type is required.' );
		}

		if ( null !== $this->targetId && $this->targetId <= 0 ) {
			throw new InvalidArgumentException( 'Object IDs must be positive.' );
		}
	}

	public function targetType(): string {
		return $this->targetType;
	}

	public function objectType(): string {
		return $this->targetType();
	}

	public function targetId(): ?int {
		return $this->targetId;
	}

	public function subtype(): string {
		return $this->subtype;
	}

	/**
	 * @param mixed $fallback
	 * @return mixed
	 */
	public function attribute( string $key, $fallback = null ) {
		return $this->attributes[ $key ] ?? $fallback;
	}

	/**
	 * @return array<string,mixed>
	 */
	public function attributes(): array {
		return $this->attributes;
	}

	public function cacheKey(): string {
		$attributes = $this->attributes;
		$this->sortRecursively( $attributes );
		$encoded = wp_json_encode(
			$attributes,
			JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION
		);

		if ( false === $encoded ) {
			throw new \UnexpectedValueException( 'Provider context attributes must be JSON encodable.' );
		}

		return hash(
			'sha256',
			$this->targetType
			. '|'
			. $this->subtype
			. '|'
			. (string) $this->targetId
			. '|'
			. $encoded
		);
	}

	/**
	 * @param array<mixed> $values Context attributes.
	 */
	private function sortRecursively( array &$values ): void {
		if ( ! \ScheduledChanges\Support\PhpCompat::isList( $values ) ) {
			ksort( $values );
		}

		foreach ( $values as &$value ) {
			if ( is_array( $value ) ) {
				$this->sortRecursively( $value );
			}
		}
		unset( $value );
	}
}
