<?php
/**
 * Embed repository contract.
 *
 * Narrow persistence port for the `affilun_embeds` table defined in
 * `specifications/database-schema.md` §6. The contract mirrors the
 * shape established by {@see ProductRepository} during Phase 2 slice
 * 4a: a small CRUD surface (save / findById / findByUuid / delete)
 * plus a count helper that diagnostics and integration tests use to
 * assert pipeline outcomes.
 *
 * Design notes:
 *
 *   - The contract speaks in {@see EmbedRecord} values — a row-shaped
 *     immutable DTO — rather than Phase 3 domain entities. This keeps
 *     the persistence layer from reaching into the domain package and
 *     lets Phase 3 wrap these records in richer domain types without
 *     rewriting the repository surface.
 *
 *   - {@see self::save()} owns the insert-vs-update decision: callers
 *     pass a record that either carries an id (update) or does not
 *     (insert). Implementations assign the auto-increment id on insert
 *     and return a new record carrying the assigned id, so callers do
 *     not have to round-trip through {@see self::findById()} to learn
 *     which id was allocated.
 *
 *   - The `uuid` unique constraint from §6.3 is surfaced as a
 *     dedicated lookup ({@see self::findByUuid()}) so REST and
 *     shortcode code paths can probe for an existing embed by its
 *     externally-visible prefixed-ULID without scanning the table.
 *     Implementations MUST honor the unique constraint; a save that
 *     would violate it is a programming error and should surface
 *     verbatim from the underlying driver rather than being silently
 *     coerced.
 *
 *   - Bulk query surfaces (paginated listing, filter-by-type, cursor
 *     traversal) are deliberately NOT part of the Phase 2 contract.
 *     They will be added as dedicated methods — or a separate
 *     query-side port — when the admin list view and the reusable-
 *     definition management surfaces materialize in later phases, so
 *     the initial contract stays small and reversible.
 *
 *   - All timestamps on {@see EmbedRecord} are UTC
 *     {@see \DateTimeImmutable} instances. Implementations are
 *     responsible for converting to and from the MySQL `Y-m-d H:i:s`
 *     representation on the storage boundary; callers never see the
 *     string form through this port.
 *
 *   - Management of the `affilun_embed_items` child rows described in §7
 *     is deliberately NOT part of this contract. Embed-items
 *     persistence is a separate concern — the child rows are looked
 *     up through their own repository when it arrives — so this
 *     interface stays focused on the embed definition itself.
 *
 * Failure semantics:
 *
 *   - Database-level errors (connection loss, constraint violations,
 *     malformed rows) MUST surface as exceptions. Implementations
 *     SHOULD wrap driver-specific errors in a descriptive
 *     {@see \RuntimeException} so call sites can catch at a single
 *     boundary without importing driver classes.
 *
 *   - "Not found" is a success condition for read methods: they
 *     return null rather than throwing. This keeps the common case
 *     (probe for an existing record, branch on absence) allocation-
 *     free and free of exception control flow.
 *
 *   - Delete is idempotent. {@see self::delete()} returns true when a
 *     row was removed and false when none matched, matching the
 *     `$wpdb->delete()` convention; neither outcome is an error.
 *
 * @package Affilun\Infrastructure\Persistence
 */

declare(strict_types=1);

namespace Affilun\Infrastructure\Persistence;

use RuntimeException;

/**
 * Persistence port for `affilun_embeds` rows.
 *
 * See the file-level docblock for the contract's scope, the record
 * type it exchanges, and its insert-vs-update / not-found / delete
 * semantics.
 */
interface EmbedRepository {

	/**
	 * Persist the given record.
	 *
	 * Implementations decide insert vs. update based on whether the
	 * supplied record carries an id:
	 *
	 *   - Null id: insert. The implementation assigns the auto-
	 *     increment primary key and returns a new record carrying
	 *     the assigned id. The returned record reflects any other
	 *     column defaults the database applied (for example
	 *     timestamps normalized to the storage precision).
	 *
	 *   - Non-null id: update. The implementation writes every column
	 *     on the record to the matching row. The returned record is
	 *     the canonical post-save shape; callers MUST use the returned
	 *     record (not the input) for any subsequent operation so
	 *     `updated_at` and any implementation-normalized values are
	 *     not silently dropped.
	 *
	 * A save that targets a non-null id which no longer exists in the
	 * database MUST raise a {@see RuntimeException} rather than
	 * silently inserting a new row with a caller-controlled id — the
	 * application's update-after-delete race should be a visible
	 * error, not a data-integrity surprise.
	 *
	 * @param EmbedRecord $record Record to persist.
	 *
	 * @return EmbedRecord The post-save record, always carrying a
	 *                     non-null id.
	 *
	 * @throws RuntimeException When the underlying driver reports an
	 *                          error, when the `uuid` unique
	 *                          constraint is violated, or when an
	 *                          update targets a row that no longer
	 *                          exists.
	 */
	public function save( EmbedRecord $record ): EmbedRecord;

	/**
	 * Look up a record by its auto-increment primary key.
	 *
	 * Returns null when no row matches. Non-positive ids are a
	 * programming error and implementations MAY raise
	 * {@see \InvalidArgumentException}; callers that only have an
	 * optional id in hand should filter before calling.
	 *
	 * @param int $id Primary key to look up.
	 *
	 * @throws RuntimeException When the underlying driver reports an
	 *                          error.
	 */
	public function findById( int $id ): ?EmbedRecord;

	/**
	 * Look up a record by its `uuid` unique key from §6.3.
	 *
	 * Returns null when no row matches. Empty strings are a
	 * programming error and implementations MAY raise
	 * {@see \InvalidArgumentException}.
	 *
	 * @param string $uuid Prefixed-ULID external identifier.
	 *
	 * @throws RuntimeException When the underlying driver reports an
	 *                          error.
	 */
	public function findByUuid( string $uuid ): ?EmbedRecord;

	/**
	 * Delete the row identified by the given id.
	 *
	 * Idempotent: returns true when a row was removed, false when no
	 * row matched. Neither outcome is an error.
	 *
	 * Non-positive ids are a programming error and implementations
	 * MAY raise {@see \InvalidArgumentException}.
	 *
	 * Note: per §14.2, callers are responsible for removing any
	 * `affilun_embed_items` child rows before (or alongside) deleting the
	 * parent embed. This repository does not cascade because the
	 * child-row repository is a separate port.
	 *
	 * @param int $id Primary key of the row to delete.
	 *
	 * @throws RuntimeException When the underlying driver reports an
	 *                          error.
	 */
	public function delete( int $id ): bool;

	/**
	 * Total number of rows currently stored in `affilun_embeds`.
	 *
	 * Used by diagnostics and integration tests to assert on the
	 * pipeline outcome without enumerating rows. Implementations
	 * SHOULD back this with a `SELECT COUNT(*)` query rather than
	 * materializing every row.
	 *
	 * @throws RuntimeException When the underlying driver reports an
	 *                          error.
	 */
	public function count(): int;
}
