<?php
/**
 * List-sync-jobs result value object.
 *
 * Phase 11 slice 11e-A immutable VO that wraps the §11.3
 * `GET /affilun/v1/sync/jobs` response `data` shape documented in
 * `specifications/rest-api-spec.md`. The VO carries the
 * wire-projected row slice (already translated from DB to wire
 * vocabulary by {@see ListSyncJobsUseCase::projectJob()} — in
 * particular DB `pending` → wire `queued` per §6.0) plus the
 * pagination axes echoed back from the originating
 * {@see \Affilun\Infrastructure\Persistence\SyncJobListQuery}
 * so REST consumers can render the pagination envelope without
 * re-issuing the query.
 *
 * The use case populates `pageCount` from
 * {@see \Affilun\Infrastructure\Persistence\SyncJobListResult::pageCount()}
 * which already implements the "0 pages when total is 0" empty-
 * result convention; the constructor only enforces the non-
 * negative invariant.
 *
 * @package Affilun\Application\SyncJobs\UseCase
 */

declare(strict_types=1);

namespace Affilun\Application\SyncJobs\UseCase;

use InvalidArgumentException;

/**
 * Immutable result of {@see ListSyncJobsUseCase::execute()}.
 *
 * See the file-level docblock for the §11.3 envelope shape and
 * the wire-vocabulary projection contract that produces the
 * contained items.
 */
final class ListSyncJobsResult {

	/**
	 * Wire-projected row slice in newest-first order.
	 *
	 * Each entry is the §6.5 `SyncJob` shape produced by
	 * {@see ListSyncJobsUseCase::projectJob()} — DB `pending` has
	 * already been remapped to wire `queued`.
	 *
	 * @var list<array<string, mixed>>
	 */
	private array $items;

	/**
	 * 1-based page number echoed back from the originating query.
	 */
	private int $page;

	/**
	 * Records per page echoed back from the originating query.
	 */
	private int $perPage;

	/**
	 * Total matching rows across every page (after filtering,
	 * before pagination).
	 */
	private int $total;

	/**
	 * Total page count derived from `$total` / `$perPage`.
	 */
	private int $pageCount;

	/**
	 * Construct the result with explicit values for every axis.
	 *
	 * @param list<array<string, mixed>> $items     Wire-projected row slice.
	 * @param int                        $page      1-based page number (>= 0).
	 * @param int                        $perPage   Records per page (>= 0).
	 * @param int                        $total     Total matching rows (>= 0).
	 * @param int                        $pageCount Total page count (>= 0).
	 *
	 * @throws InvalidArgumentException When any int axis is negative.
	 */
	public function __construct(
		array $items,
		int $page,
		int $perPage,
		int $total,
		int $pageCount
	) {
		if ( $page < 0 ) {
			$message = sprintf( 'ListSyncJobsResult: page must be >= 0, got %d.', $page );
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new InvalidArgumentException( $message );
		}

		if ( $perPage < 0 ) {
			$message = sprintf( 'ListSyncJobsResult: perPage must be >= 0, got %d.', $perPage );
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new InvalidArgumentException( $message );
		}

		if ( $total < 0 ) {
			$message = sprintf( 'ListSyncJobsResult: total must be >= 0, got %d.', $total );
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new InvalidArgumentException( $message );
		}

		if ( $pageCount < 0 ) {
			$message = sprintf( 'ListSyncJobsResult: pageCount must be >= 0, got %d.', $pageCount );
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new InvalidArgumentException( $message );
		}

		$this->items     = array_values( $items );
		$this->page      = $page;
		$this->perPage   = $perPage;
		$this->total     = $total;
		$this->pageCount = $pageCount;
	}

	/**
	 * Wire-projected row slice in newest-first order.
	 *
	 * @return list<array<string, mixed>>
	 */
	public function items(): array {
		return $this->items;
	}

	/**
	 * 1-based page number echoed back from the originating query.
	 */
	public function page(): int {
		return $this->page;
	}

	/**
	 * Records per page echoed back from the originating query.
	 */
	public function perPage(): int {
		return $this->perPage;
	}

	/**
	 * Total matching rows across every page.
	 */
	public function total(): int {
		return $this->total;
	}

	/**
	 * Total page count derived from `$total` / `$perPage`.
	 */
	public function pageCount(): int {
		return $this->pageCount;
	}

	/**
	 * Render the §11.3 success envelope `data` shape:
	 *
	 *   `array(
	 *       'items'      => list<array<string, mixed>>,
	 *       'page'       => int,
	 *       'per_page'   => int,
	 *       'total'      => int,
	 *       'page_count' => int,
	 *   )`
	 *
	 * @return array{
	 *     items: list<array<string, mixed>>,
	 *     page: int,
	 *     per_page: int,
	 *     total: int,
	 *     page_count: int,
	 * }
	 */
	public function toArray(): array {
		return array(
			'items'      => $this->items,
			'page'       => $this->page,
			'per_page'   => $this->perPage,
			'total'      => $this->total,
			'page_count' => $this->pageCount,
		);
	}
}
