<?php
/**
 * Transaction manager contract.
 *
 * Narrow persistence-layer port that brackets a unit of work with the
 * underlying driver's transaction primitives — `START TRANSACTION` on
 * entry, `COMMIT` on success, `ROLLBACK` on failure. The contract is
 * intentionally limited to the single
 * {@see self::transactional()} surface so the production
 * implementation ({@see WpdbTransactionManager}) and the in-memory
 * test double ({@see InMemoryTransactionManager}) stay small,
 * auditable, and substitutable.
 *
 * Design notes:
 *
 *   - The unit of work is supplied as a `callable` rather than a
 *     dedicated command object. Callers compose the work inline at
 *     the call site, which keeps the port free of project-specific
 *     command shapes and lets Phase 9 sync code mix repository writes
 *     with domain-level orchestration without inventing intermediate
 *     types.
 *
 *   - The callable's return value is forwarded back to the caller
 *     verbatim. This lets the `transactional()` boundary stay
 *     value-transparent: a caller that wants to return a freshly
 *     persisted record from inside the transaction does not have to
 *     stash it in an outer variable.
 *
 *   - Nested transactions are deliberately NOT supported by the
 *     Phase 2 contract. Calling {@see self::transactional()} from
 *     within an already-open transaction MUST raise a
 *     {@see RuntimeException} rather than starting a savepoint.
 *     Rationale: MySQL's `START TRANSACTION` semantics for the nested
 *     case are subtle (auto-commit toggling, savepoint naming,
 *     interaction with `$wpdb`'s own connection state) and Phase 2
 *     has no consumer that needs nesting. Phase 9 (sync worker) may
 *     extend the contract to add savepoint support when a concrete
 *     use case forces the design choice; today, surfacing nesting as
 *     a visible error keeps the contract small and forces callers to
 *     reason about transaction scope at the boundary rather than
 *     hiding it.
 *
 *   - On exception the implementation MUST roll back the transaction
 *     before re-throwing. The original exception MUST propagate
 *     verbatim — implementations MUST NOT wrap, swallow, or re-shape
 *     it — so call sites can match on the original type. A failure
 *     during ROLLBACK itself (driver error, connection loss) MAY be
 *     surfaced as a {@see RuntimeException} chained to the original
 *     exception via `$previous`, so neither error is lost.
 *
 *   - The boundary is intentionally driver-agnostic. The
 *     {@see WpdbTransactionManager} implementation issues
 *     `$wpdb->query()` against the WordPress database connection;
 *     the in-memory double records the lifecycle transitions for
 *     test assertions without touching a real database. Both honor
 *     the same nesting rule and the same exception-propagation
 *     contract.
 *
 * Failure semantics summary:
 *
 *   - `$work` returns normally → `COMMIT`, return value forwarded.
 *   - `$work` throws → `ROLLBACK`, original exception re-thrown.
 *   - Nested call → {@see RuntimeException} before any driver call.
 *   - Driver error during `START TRANSACTION` / `COMMIT` /
 *     `ROLLBACK` → {@see RuntimeException} carrying the driver
 *     message verbatim, with the original `$work` exception (when
 *     applicable) chained via `$previous`.
 *
 * @package Affilun\Infrastructure\Persistence
 */

declare(strict_types=1);

namespace Affilun\Infrastructure\Persistence;

use RuntimeException;
use Throwable;

/**
 * Persistence port that brackets a unit of work with database
 * transaction primitives.
 *
 * See the file-level docblock for the boundary's value-transparent
 * forwarding contract, the deliberate non-support of nesting in Phase
 * 2, and the exception-propagation rules implementations must honor.
 */
interface TransactionManager {

	/**
	 * Execute the given callable inside a database transaction.
	 *
	 * Lifecycle:
	 *
	 *   1. Open a fresh transaction. If a transaction is already
	 *      active on this manager instance, raise
	 *      {@see RuntimeException} before issuing any driver call —
	 *      Phase 2 does not support nested transactions.
	 *   2. Invoke `$work` with no arguments.
	 *   3. If `$work` returns normally, commit and forward the
	 *      return value verbatim to the caller.
	 *   4. If `$work` throws any {@see Throwable}, roll back the
	 *      transaction and re-throw the original exception verbatim
	 *      (no wrapping, no swallowing). A subsequent driver error
	 *      during ROLLBACK MAY surface as a {@see RuntimeException}
	 *      chained to the original exception via `$previous`.
	 *
	 * The callable is executed exactly once per call. Implementations
	 * MUST NOT retry on failure — retry / backoff policy is the
	 * caller's concern (per the Phase 9 sync slice's retry budget on
	 * `affilun_sync_jobs`).
	 *
	 * @template TReturn
	 *
	 * @param callable():TReturn $work Unit of work to execute.
	 *
	 * @return TReturn The value returned by `$work`, forwarded verbatim.
	 *
	 * @throws RuntimeException When called while a transaction is
	 *                          already active on this manager
	 *                          instance, or when the underlying
	 *                          driver reports an error during
	 *                          `START TRANSACTION` / `COMMIT` /
	 *                          `ROLLBACK`.
	 * @throws Throwable        Any exception raised by `$work` is
	 *                          re-thrown verbatim after the
	 *                          transaction has been rolled back.
	 */
	public function transactional( callable $work );

	/**
	 * Report whether a transaction is currently active on this
	 * manager instance.
	 *
	 * Exposed so test assertions can check the lifecycle state
	 * without driving a fresh `transactional()` call. Production
	 * code SHOULD NOT branch on this method — the
	 * {@see self::transactional()} boundary is the canonical entry
	 * point and the only way to enter a transactional scope.
	 */
	public function inTransaction(): bool;
}
