<?php
/**
 * Application-layer "product not found" exception.
 *
 * Raised by Phase 6 product UseCases (specifically
 * {@see \Affilun\Application\Product\UseCase\GetProductUseCase})
 * when the persistence layer reports no row matches the supplied
 * product id. The exception carries the offending id so the
 * Phase 6b REST controller (added in a subsequent slice) can map
 * it onto the §8.3 404 response without re-deriving the value
 * from the request URL.
 *
 * Design notes:
 *
 *   - This exception is deliberately distinct from
 *     {@see \Affilun\Domain\Provider\Exception\ProductNotFoundException}.
 *     The Domain-layer counterpart represents an upstream provider
 *     reporting that a `ProviderProductId` does not exist; this
 *     Application-layer counterpart represents the local
 *     `affilun_products` table not carrying a row with the supplied
 *     auto-increment primary key. The two failure modes map to
 *     different REST envelopes (provider-attribution vs. local
 *     persistence) and SHOULD remain distinguishable by type.
 *
 *   - The class extends {@see \RuntimeException} (not the Domain
 *     `ProviderException` hierarchy) because the failure has no
 *     provider attribution: a missing row is purely a local
 *     persistence outcome and the §10.2 provider error codes
 *     would not apply.
 *
 *   - The class is final because the REST mapping layer pattern-
 *     matches on the exact type. Consumers that need additional
 *     context (audit ids, locale-specific messages) should layer
 *     a wrapper exception around this one rather than subclass.
 *
 * @package Affilun\Application\Product\Exception
 */

declare(strict_types=1);

namespace Affilun\Application\Product\Exception;

use RuntimeException;
use Throwable;

/**
 * Application-layer exception signalling the supplied product id
 * has no matching row in the `affilun_products` table.
 *
 * See the file-level docblock for the rationale for keeping this
 * type distinct from the Domain-layer
 * {@see \Affilun\Domain\Provider\Exception\ProductNotFoundException}
 * and for the REST-mapping role the carried id plays.
 */
final class ProductNotFoundException extends RuntimeException {

	/**
	 * Local product id that was looked up.
	 */
	private int $id;

	/**
	 * Construct a not-found exception with explicit values for
	 * every cross-cutting field.
	 *
	 * @param string         $message  Human-readable summary.
	 * @param int            $id       Local product id that was looked up.
	 * @param Throwable|null $previous Underlying cause, or null.
	 */
	public function __construct( string $message, int $id, ?Throwable $previous = null ) {
		parent::__construct( $message, 0, $previous );
		$this->id = $id;
	}

	/**
	 * Local product id that was looked up.
	 */
	public function id(): int {
		return $this->id;
	}

	/**
	 * Convenience factory for the common "lookup by id failed" case.
	 *
	 * Produces the canonical message string the REST mapper relies
	 * on when no caller-supplied summary is available.
	 *
	 * @param int $id Local product id that was looked up.
	 */
	public static function forId( int $id ): self {
		return new self(
			sprintf( 'Product with id %d was not found.', $id ),
			$id
		);
	}
}
