<?php
/**
 * Validation report aggregate.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Validation;

use ArrayIterator;
use Countable;
use IteratorAggregate;
use Traversable;

/**
 * Immutable collection of informational, warning, and blocking issues.
 *
 * @implements IteratorAggregate<int,ValidationIssue>
 */
final class ValidationReport implements Countable, IteratorAggregate {
	/**
	 * @var ValidationIssue[]
	 */
	private array $issues = array();
	/**
	 * @param ValidationIssue[] $issues Validation issues.
	 */
	public function __construct( array $issues = array() ) {
		$this->issues = $issues;
		foreach ( $this->issues as $issue ) {
			if ( ! $issue instanceof ValidationIssue ) {
				throw new \InvalidArgumentException( 'Validation reports accept only ValidationIssue objects.' );
			}
		}
	}

	public static function valid(): self {
		return new self();
	}

	public function count(): int {
		return count( $this->issues );
	}

	/**
	 * @return Traversable<int,ValidationIssue>
	 */
	public function getIterator(): Traversable {
		return new ArrayIterator( $this->issues );
	}

	public function isValid(): bool {
		return array() === $this->blockingErrors();
	}

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

	/**
	 * @return ValidationIssue[]
	 */
	public function blockingErrors(): array {
		return $this->atLevel( ValidationLevel::BLOCKING_ERROR );
	}

	/**
	 * @return ValidationIssue[]
	 */
	public function warnings(): array {
		return $this->atLevel( ValidationLevel::WARNING );
	}

	/**
	 * @return ValidationIssue[]
	 */
	public function informational(): array {
		return $this->atLevel( ValidationLevel::INFORMATIONAL );
	}

	public function merge( self ...$reports ): self {
		$issues = $this->issues;

		foreach ( $reports as $report ) {
			array_push( $issues, ...$report->issues() );
		}

		return new self( $issues );
	}

	/**
	 * @return array<string,mixed>
	 */
	public function toArray(): array {
		$issues = $this->issues;
		usort(
			$issues,
			static function ( ValidationIssue $left, ValidationIssue $right ): int {
				return array(
					$left->level(),
					$left->path(),
					$left->code(),
					$left->message(),
				) <=> array(
					$right->level(),
					$right->path(),
					$right->code(),
					$right->message(),
				);
			}
		);

		return array(
			'valid'                => $this->isValid(),
			'blocking_error_count' => count( $this->blockingErrors() ),
			'warning_count'        => count( $this->warnings() ),
			'informational_count'  => count( $this->informational() ),
			'issues'               => array_map(
				static fn ( ValidationIssue $issue ): array => $issue->toArray(),
				$issues
			),
		);
	}

	/**
	 * @param array<string,mixed> $data Serialized report.
	 */
	public static function fromArray( array $data ): self {
		$issues = array();

		foreach ( is_array( $data['issues'] ?? null ) ? $data['issues'] : array() as $issue ) {
			if ( is_array( $issue ) ) {
				$issues[] = ValidationIssue::fromArray( $issue );
			}
		}

		return new self( $issues );
	}

	/**
	 * @return ValidationIssue[]
	 */
	private function atLevel( string $level ): array {
		return array_values(
			array_filter(
				$this->issues,
				static fn ( ValidationIssue $issue ): bool => $issue->level() === $level
			)
		);
	}
}
