<?php
/**
 * Get product use case.
 *
 * Phase 6 Application-layer UseCase that drives the §8.3
 * `GET /affilun/v1/products/{id}` endpoint. The class accepts the
 * controller-supplied auto-increment primary key and returns the
 * matching
 * {@see \Affilun\Infrastructure\Persistence\ProductRecord}
 * from the persistence layer, raising
 * {@see ProductNotFoundException} when no row matches so the REST
 * mapper can render the §8.3 404 response without re-deriving the
 * lookup id from the request URL.
 *
 * Behavior notes:
 *
 *   - Non-positive ids (zero, negative) are a programming error in
 *     the controller — the §5.3 path-parameter validation should
 *     have rejected them before reaching this UseCase. The method
 *     surfaces the violation as
 *     {@see \InvalidArgumentException} carrying the offending id
 *     so the failure is visible at the type-system boundary
 *     rather than silently producing a "not found" response for
 *     an id that could never have matched any row.
 *
 *   - "Not found" — the repository returns null — is NOT encoded
 *     as a sentinel return on this UseCase. Instead the method
 *     raises {@see ProductNotFoundException} carrying the
 *     looked-up id; the REST mapper pattern-matches on the
 *     exception type to produce the §8.3 404 envelope. This
 *     mirrors the §6.3 "lookup-not-found surfaces as exception"
 *     convention the Domain layer applies to provider lookups.
 *
 *   - The Application-layer
 *     {@see ProductNotFoundException} is deliberately distinct
 *     from the Domain-layer
 *     {@see \Affilun\Domain\Provider\Exception\ProductNotFoundException}.
 *     The Domain counterpart represents an upstream provider
 *     reporting a `ProviderProductId` does not exist; this
 *     Application counterpart represents the local
 *     `affilun_products` table not carrying a row with the supplied
 *     primary key. The two map to different REST envelopes and
 *     SHOULD remain distinguishable by type.
 *
 * @package Affilun\Application\Product\UseCase
 */

declare(strict_types=1);

namespace Affilun\Application\Product\UseCase;

use Affilun\Application\Product\Exception\ProductNotFoundException;
use Affilun\Infrastructure\Persistence\ProductRecord;
use Affilun\Infrastructure\Persistence\ProductRepository;
use InvalidArgumentException;

/**
 * Phase 6 UseCase that drives the §8.3 `GET /affilun/v1/products/{id}`
 * endpoint.
 *
 * See the file-level docblock for the "not found surfaces as
 * exception" contract that binds the REST mapper, and for the
 * rationale that keeps the Application-layer
 * {@see ProductNotFoundException} distinct from the Domain-layer
 * counterpart.
 */
final class GetProductUseCase {

	/**
	 * Persistence port that backs the §8.3 lookup.
	 */
	private ProductRepository $repository;

	/**
	 * Construct the UseCase with an explicit repository binding.
	 *
	 * @param ProductRepository $repository Persistence port.
	 */
	public function __construct( ProductRepository $repository ) {
		$this->repository = $repository;
	}

	/**
	 * Execute the §8.3 lookup dispatch.
	 *
	 * @param int $id Local product id (1-based auto-increment).
	 *
	 * @return ProductRecord The record matching `$id`.
	 *
	 * @throws InvalidArgumentException When `$id` is below 1.
	 * @throws ProductNotFoundException When no row matches `$id`.
	 */
	public function execute( int $id ): ProductRecord {
		if ( $id < 1 ) {
			$message = sprintf(
				'GetProductUseCase: id must be >= 1; got %d.',
				$id
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new InvalidArgumentException( $message );
		}

		$record = $this->repository->findById( $id );
		if ( $record === null ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw ProductNotFoundException::forId( $id );
		}

		return $record;
	}
}
