<?php
/**
 * Repository-backed audit-event writer.
 *
 * Phase 2 (KI-006 follow-up) production {@see AuditEventWriter}
 * implementation that stamps the supplied {@see AuditEventDraft}
 * with the writer's injected {@see Clock} "now" instant and
 * persists the resulting {@see AuditEventRecord} through the
 * shared
 * {@see \Affilun\Infrastructure\Persistence\AuditEventRepository}.
 *
 * Design notes:
 *
 *   - The writer is the canonical observation boundary for the
 *     `createdAt` slot: every audit row gets a clock the
 *     persistence layer can trust rather than a caller-supplied
 *     instant that could drift across long-running call sites
 *     (e.g. a settings save that began at T0 but only emits the
 *     audit row after a slow upstream API call). Drafts therefore
 *     do not expose a timestamp slot, and this writer is the only
 *     code path that produces one.
 *
 *   - The clock is injected at construction time so tests can
 *     drive the writer with a frozen
 *     {@see \Affilun\Tests\Unit\Infrastructure\Cache\Support\MutableTestClock}
 *     and assert the persisted row carries the exact instant the
 *     test pinned. Production wiring uses
 *     {@see \Affilun\Infrastructure\Clock\SystemClock}.
 *
 *   - The encode-and-persist path is intentionally tiny: the
 *     draft already enforces every column-shape invariant the
 *     §10.2 schema requires (non-empty required strings, JSON
 *     object shape on optional context slots), and the
 *     repository's INSERT-only `save()` enforces the §10.1
 *     append-only invariant. The writer therefore performs no
 *     additional validation — re-validating would shadow the
 *     responsibility split documented on
 *     {@see AuditEventWriter}.
 *
 *   - Persistence failures (driver error, capacity exhaustion)
 *     surface from {@see \Affilun\Infrastructure\Persistence\AuditEventRepository::save()}
 *     as {@see RuntimeException} and propagate up through this
 *     method unchanged. The writer does NOT swallow because the
 *     call site is the correct boundary for the "audit emit
 *     failed but the business operation succeeded; tolerate or
 *     escalate?" decision (per the {@see AuditEventWriter}
 *     contract docblock).
 *
 *   - The writer is `final` and stateless beyond its two
 *     collaborators (clock + repository). A single instance is
 *     safe to share across every call site through the registry
 *     binding {@see \Affilun\Presentation\Rest\AuditServiceProvider}
 *     publishes.
 *
 * @package Affilun\Application\Audit
 */

declare(strict_types=1);

namespace Affilun\Application\Audit;

use Affilun\Domain\Clock;
use Affilun\Infrastructure\Persistence\AuditEventRecord;
use Affilun\Infrastructure\Persistence\AuditEventRepository;
use DateTimeZone;

/**
 * Repository-backed {@see AuditEventWriter} implementation.
 *
 * See the file-level docblock for the clock-stamping rule, the
 * minimal encode-and-persist responsibility, and the failure-
 * propagation policy that keeps the call site in charge of
 * "tolerate or escalate" decisions on persistence failure.
 */
final class RepositoryAuditEventWriter implements AuditEventWriter {

	/**
	 * Persistence port the writer hands every draft to.
	 *
	 * Held by reference rather than instantiated internally so
	 * production wiring (the registry binding) and tests
	 * (in-memory double) can share the writer surface without
	 * touching its construction.
	 */
	private AuditEventRepository $events;

	/**
	 * Clock port that produces the per-call `createdAt` instant.
	 *
	 * The writer is the canonical observation boundary for the
	 * audit `createdAt` slot, so every persisted row carries a
	 * clock the persistence layer can trust (see the file-level
	 * docblock for the rationale).
	 */
	private Clock $clock;

	/**
	 * Construct the writer with explicit collaborator injection.
	 *
	 * @param AuditEventRepository $events Persistence port the
	 *                                     writer persists drafts
	 *                                     through.
	 * @param Clock                $clock  Clock that produces the
	 *                                     per-call `createdAt`
	 *                                     instant.
	 */
	public function __construct( AuditEventRepository $events, Clock $clock ) {
		$this->events = $events;
		$this->clock  = $clock;
	}

	/**
	 * Persist the supplied draft and return the post-save
	 * {@see AuditEventRecord} carrying the database-assigned id.
	 *
	 * Stamps `createdAt` from {@see Clock::now()} (normalized to
	 * UTC so the persistence layer's `Y-m-d H:i:s` MySQL
	 * `DATETIME` storage is always wall-clock UTC), encodes the
	 * draft into a fresh {@see AuditEventRecord} (id = null), and
	 * hands it to {@see AuditEventRepository::save()}. The §10.1
	 * append-only invariant is enforced inside `save()`; this
	 * writer never produces a record that already carries an id.
	 *
	 * Persistence failures bubble out unchanged so the call site
	 * can decide on log-and-continue vs. escalate (see the
	 * {@see AuditEventWriter} file-level docblock).
	 *
	 * @param AuditEventDraft $draft Validated audit-event draft.
	 */
	public function record( AuditEventDraft $draft ): AuditEventRecord {
		$now = $this->clock->now()->setTimezone( new DateTimeZone( 'UTC' ) );

		$record = new AuditEventRecord(
			null,
			$draft->eventType(),
			$draft->auditKind(),
			$draft->actorType(),
			$draft->actorId(),
			$draft->targetType(),
			$draft->targetId(),
			$draft->action(),
			$draft->result(),
			$draft->safeSummary(),
			$draft->changedFieldsJson(),
			$draft->contextJson(),
			$now
		);

		return $this->events->save( $record );
	}
}
