<?php
/**
 * Canonical field definition.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Field;

use InvalidArgumentException;

/**
 * Describes one schedulable canonical field path and its required behavior.
 */
final class FieldDefinition {
	private string $id;
	private string $label;
	private string $description;
	private string $owner;
	private string $provider;
	/**
	 * @var string[]
	 */
	private array $scope;
	/**
	 * @var string[]
	 */
	private array $subtypes;
	private string $dataType;
	private string $uiComponent;
	private bool $readable    = true;
	private bool $writable    = false;
	private bool $schedulable = false;
	private bool $reversible  = false;
	private bool $sensitive   = false;
	/**
	 * @var string[]
	 */
	private array $dependencies      = array();
	private string $conflictStrategy = 'fail';
	/**
	 * @var array<string, mixed>
	 */
	private array $validationRules = array();
	private string $capability     = 'edit_post';
	/**
	 * @var string[]
	 */
	private array $sideEffects = array();
	/**
	 * @var string[]
	 */
	private array $cacheInvalidation = array();
	/**
	 * @var array<string, mixed>
	 */
	private array $normalization = array();
	/**
	 * @var array<string, mixed>
	 */
	private array $display = array();
	/**
	 * @param string[]            $scope Supported object types.
	 * @param string[]            $subtypes Supported target subtypes.
	 * @param string[]            $dependencies Canonical dependency paths.
	 * @param array<string,mixed> $validationRules Declarative validation rules.
	 * @param string[]            $sideEffects Declared side effects.
	 * @param string[]            $cacheInvalidation Cache groups or indexes to invalidate.
	 * @param array<string,mixed> $normalization Normalization options.
	 * @param array<string,mixed> $display Display metadata for recording/review UIs.
	 */
	public function __construct(
		string $id,
		string $label,
		string $description,
		string $owner,
		string $provider,
		array $scope,
		array $subtypes,
		string $dataType,
		string $uiComponent,
		bool $readable = true,
		bool $writable = false,
		bool $schedulable = false,
		bool $reversible = false,
		bool $sensitive = false,
		array $dependencies = array(),
		string $conflictStrategy = 'fail',
		array $validationRules = array(),
		string $capability = 'edit_post',
		array $sideEffects = array(),
		array $cacheInvalidation = array(),
		array $normalization = array(),
		array $display = array()
	) {
		$this->id                = $id;
		$this->label             = $label;
		$this->description       = $description;
		$this->owner             = $owner;
		$this->provider          = $provider;
		$this->scope             = $scope;
		$this->subtypes          = $subtypes;
		$this->dataType          = $dataType;
		$this->uiComponent       = $uiComponent;
		$this->readable          = $readable;
		$this->writable          = $writable;
		$this->schedulable       = $schedulable;
		$this->reversible        = $reversible;
		$this->sensitive         = $sensitive;
		$this->dependencies      = $dependencies;
		$this->conflictStrategy  = $conflictStrategy;
		$this->validationRules   = $validationRules;
		$this->capability        = $capability;
		$this->sideEffects       = $sideEffects;
		$this->cacheInvalidation = $cacheInvalidation;
		$this->normalization     = $normalization;
		$this->display           = $this->normalizeDisplay( $display );
		if ( ! preg_match( '/^[a-z][a-z0-9_]*(?:\.[a-z0-9_{}-]+)+$/', $this->id ) ) {
			throw new InvalidArgumentException( 'Field IDs must be lowercase canonical paths.' );
		}

		if ( '' === trim( $this->label ) || '' === trim( $this->owner ) || '' === trim( $this->provider ) ) {
			throw new InvalidArgumentException( 'Field label, owner, and provider are required.' );
		}

		if ( array() === $this->scope ) {
			throw new InvalidArgumentException( 'A field must support at least one object scope.' );
		}

		if ( $this->schedulable && ! $this->writable ) {
			throw new InvalidArgumentException( 'A schedulable field must be writable.' );
		}

		if ( $this->reversible && ! $this->writable ) {
			throw new InvalidArgumentException( 'A reversible field must be writable.' );
		}
	}

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

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

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

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

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

	/**
	 * @return string[]
	 */
	public function scope(): array {
		return $this->scope;
	}

	/**
	 * @return string[]
	 */
	public function subtypes(): array {
		return $this->subtypes;
	}

	/**
	 * @return string[]
	 */
	public function productTypes(): array {
		return $this->subtypes();
	}

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

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

	public function readable(): bool {
		return $this->readable;
	}

	public function writable(): bool {
		return $this->writable;
	}

	public function schedulable(): bool {
		return $this->schedulable;
	}

	public function reversible(): bool {
		return $this->reversible;
	}

	public function sensitive(): bool {
		return $this->sensitive;
	}

	/**
	 * @return string[]
	 */
	public function dependencies(): array {
		return $this->dependencies;
	}

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

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

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

	/**
	 * @return string[]
	 */
	public function sideEffects(): array {
		return $this->sideEffects;
	}

	/**
	 * @return string[]
	 */
	public function cacheInvalidation(): array {
		return $this->cacheInvalidation;
	}

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

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

	/**
	 * Returns a serialization-safe field schema.
	 *
	 * @return array<string,mixed>
	 */
	public function toArray(): array {
		return array(
			'id'                 => $this->id,
			'label'              => $this->label,
			'description'        => $this->description,
			'owner'              => $this->owner,
			'provider'           => $this->provider,
			'scope'              => $this->scope,
			'target_subtypes'    => $this->subtypes,
			'data_type'          => $this->dataType,
			'ui_component'       => $this->uiComponent,
			'readable'           => $this->readable,
			'writable'           => $this->writable,
			'schedulable'        => $this->schedulable,
			'reversible'         => $this->reversible,
			'sensitive'          => $this->sensitive,
			'dependencies'       => $this->dependencies,
			'conflict_strategy'  => $this->conflictStrategy,
			'validation_rules'   => $this->validationRules,
			'capability'         => $this->capability,
			'side_effects'       => $this->sideEffects,
			'cache_invalidation' => $this->cacheInvalidation,
			'normalization'      => $this->normalization,
			'display'            => $this->display,
		);
	}

	/**
	 * Rehydrates a field definition from a cached or external schema.
	 *
	 * @param array<string,mixed> $definition Definition values.
	 */
	public static function fromArray( array $definition ): self {
		return new self(
			(string) ( $definition['id'] ?? '' ),
			(string) ( $definition['label'] ?? '' ),
			(string) ( $definition['description'] ?? '' ),
			(string) ( $definition['owner'] ?? '' ),
			(string) ( $definition['provider'] ?? '' ),
			self::stringList( $definition['scope'] ?? array() ),
			self::stringList( $definition['target_subtypes'] ?? array() ),
			\ScheduledChanges\Support\EnumValue::from( \ScheduledChanges\Field\FieldDataType::class, (string) ( $definition['data_type'] ?? FieldDataType::MIXED ) ),
			(string) ( $definition['ui_component'] ?? 'text' ),
			(bool) ( $definition['readable'] ?? true ),
			(bool) ( $definition['writable'] ?? false ),
			(bool) ( $definition['schedulable'] ?? false ),
			(bool) ( $definition['reversible'] ?? false ),
			(bool) ( $definition['sensitive'] ?? false ),
			self::stringList( $definition['dependencies'] ?? array() ),
			(string) ( $definition['conflict_strategy'] ?? 'fail' ),
			is_array( $definition['validation_rules'] ?? null ) ? $definition['validation_rules'] : array(),
			(string) ( $definition['capability'] ?? 'edit_post' ),
			self::stringList( $definition['side_effects'] ?? array() ),
			self::stringList( $definition['cache_invalidation'] ?? array() ),
			is_array( $definition['normalization'] ?? null ) ? $definition['normalization'] : array(),
			is_array( $definition['display'] ?? null ) ? $definition['display'] : array()
		);
	}

	/**
	 * @param mixed $values Candidate string list.
	 * @return string[]
	 */
	private static function stringList( $values ): array {
		if ( ! is_array( $values ) ) {
			return array();
		}

		return array_values(
			array_map(
				static fn ( $value ): string => (string) $value,
				$values
			)
		);
	}

	/**
	 * @param array<string,mixed> $display Display metadata.
	 * @return array<string,mixed>
	 */
	private function normalizeDisplay( array $display ): array {
		$normalized = array();

		foreach (
			array(
				'group_key',
				'group_label',
				'provider_label',
				'help_text',
				'warning_text',
			) as $key
		) {
			if ( isset( $display[ $key ] ) && is_scalar( $display[ $key ] ) ) {
				$normalized[ $key ] = trim( (string) $display[ $key ] );
			}
		}

		if ( isset( $display['sort_order'] ) && is_numeric( $display['sort_order'] ) ) {
			$normalized['sort_order'] = (int) $display['sort_order'];
		}

		return array_filter(
			$normalized,
			static fn ( $value ): bool => '' !== $value
		);
	}
}
