<?php
/**
 * Audit-event writer port.
 *
 * Phase 2 (KI-006 follow-up) Application-layer port that exposes
 * a single narrow seam for emitting `affilun_audit_events` rows from
 * any call site that produces a §10.1 audit-worthy event:
 * synchronous user actions (settings save, capability change,
 * destructive admin operation), background workers (sync job
 * terminal transition, retention purge), and lifecycle events
 * (plugin activation, schema migration). Replaces the previous
 * "no writer" gap by giving each call site one well-typed entry
 * point so the §10.2 column shape, the §10.1 append-only
 * invariant, and the {@see \Affilun\Application\Logs\LogMasker}
 * "safe summary" rule are enforced in exactly one place.
 *
 * Design notes:
 *
 *   - The contract is a one-method port. {@see self::record()}
 *     is the only operation call sites need; the writer encodes
 *     the supplied {@see AuditEventDraft} into an
 *     {@see \Affilun\Infrastructure\Persistence\AuditEventRecord}
 *     and persists it through the
 *     {@see \Affilun\Infrastructure\Persistence\AuditEventRepository}.
 *     Aggregations (per-actor recent activity, per-target diff
 *     timeline) belong on a future read-side projection, not on
 *     this port.
 *
 *   - The contract speaks in {@see AuditEventDraft} values — a
 *     row-shaped immutable VO carrying everything the §10.2
 *     schema needs except the auto-increment id and the row
 *     creation timestamp (the writer stamps `createdAt` from the
 *     injected {@see \Affilun\Domain\Clock}). Call
 *     sites that already carry a `request_id` /
 *     `correlation_id` / `job_id` for tracing build the draft
 *     with the matching `contextJson` payload so the §12.4
 *     envelope projection can lift those tracker tokens back out
 *     for the operator UI.
 *
 *   - Implementations MUST honor the §10.1 append-only invariant
 *     enforced by {@see \Affilun\Infrastructure\Persistence\AuditEventRepository::save()}:
 *     drafts always produce fresh rows, never updates. A failed
 *     write (driver error, capacity exhaustion) MUST surface as
 *     an exception so the caller can decide whether to retry,
 *     escalate, or proceed with a degraded posture.
 *
 *   - The contract makes no promise about transactional
 *     semantics. Call sites that need atomic "succeed both the
 *     business operation AND the audit emit, or roll back both"
 *     guarantees should wrap both operations in their own
 *     {@see \Affilun\Infrastructure\Persistence\TransactionManager}
 *     scope — the writer participates in any active transaction
 *     by virtue of using `$wpdb->insert()` underneath. The
 *     reverse contract (audit emit failure should NOT silently
 *     dismantle the business operation) is the call site's
 *     responsibility: most callers SHOULD log-and-continue on a
 *     {@see \RuntimeException} from the writer rather than
 *     re-raise, because losing one audit row is preferable to
 *     losing the business operation it described.
 *
 *   - Safe-summary scrubbing is the call site's responsibility,
 *     not the writer's. The {@see AuditEventDraft} constructor
 *     accepts the `safe_summary` verbatim and the writer hands
 *     it to {@see \Affilun\Infrastructure\Persistence\AuditEventRecord}
 *     unchanged. Call sites that touch secrets MUST run their
 *     summary through {@see \Affilun\Application\Logs\LogMasker}
 *     (or equivalent) BEFORE constructing the draft. This split
 *     mirrors the {@see \Affilun\Infrastructure\Persistence\LogRepository}
 *     contract for `affilun_logs` rows: the persistence boundary is
 *     not the security boundary, the application layer is.
 *
 * Failure semantics:
 *
 *   - Implementations MUST raise {@see \RuntimeException} (or a
 *     subclass) when the underlying repository fails. Wrapping
 *     in a more descriptive type (e.g. an audit-specific
 *     `AuditWriteFailedException`) is permitted but not required
 *     by the contract.
 *
 *   - Validation failures on the supplied {@see AuditEventDraft}
 *     surface from the draft constructor itself (the draft VO
 *     normalizes / validates every field at construction time).
 *     The writer therefore does NOT re-validate; it trusts the
 *     draft's invariants and persists it verbatim.
 *
 * @package Affilun\Application\Audit
 */

declare(strict_types=1);

namespace Affilun\Application\Audit;

use Affilun\Infrastructure\Persistence\AuditEventRecord;
use RuntimeException;

/**
 * Phase 2 (KI-006 follow-up) audit-event writer contract.
 *
 * See the file-level docblock for the one-method scope, the
 * append-only invariant, the safe-summary responsibility split,
 * and the failure semantics. Production wiring is provided by
 * {@see RepositoryAuditEventWriter}; tests typically inject an
 * in-memory recording double.
 */
interface AuditEventWriter {

	/**
	 * Persist the supplied audit-event draft and return the
	 * persisted record carrying the database-assigned id.
	 *
	 * Implementations:
	 *
	 *   - Stamp `createdAt` from the writer's injected
	 *     {@see \Affilun\Domain\Clock}, NOT from the
	 *     draft (the draft does not expose a timestamp slot —
	 *     the writer is the canonical observation boundary so
	 *     every audit row carries a clock the persistence layer
	 *     can trust).
	 *
	 *   - Encode the draft into a fresh
	 *     {@see AuditEventRecord} (id = null) and persist it
	 *     through the underlying
	 *     {@see \Affilun\Infrastructure\Persistence\AuditEventRepository::save()}.
	 *     The §10.1 append-only invariant is enforced there;
	 *     callers do not need to hold the persisted record alive
	 *     unless they want to use the assigned id for follow-up
	 *     observability (e.g. include it in a logs entry).
	 *
	 *   - Surface persistence failures as
	 *     {@see RuntimeException} so the caller can decide on
	 *     log-and-continue vs. escalate. Do NOT swallow the
	 *     exception inside the writer — the call site is the
	 *     correct boundary for "audit emit failed, but the
	 *     business operation succeeded; tolerate or escalate?".
	 *
	 * @param AuditEventDraft $draft Validated audit-event draft.
	 *
	 * @return AuditEventRecord Persisted record carrying the
	 *                          assigned auto-increment id.
	 *
	 * @throws RuntimeException When the underlying repository
	 *                          fails to persist the row.
	 */
	public function record( AuditEventDraft $draft ): AuditEventRecord;
}
