<?php
/**
 * Get-audit-events-summary use case.
 *
 * Phase 2 (KI-006 follow-up) Application-layer use case that
 * drives the §12.5 `GET /clb/v1/audit/events/summary` REST
 * surface documented in `specifications/rest-api-spec.md`. The
 * use case combines the per-`audit_kind` histogram half
 * ({@see AuditEventRepository::countByAuditKind()}) and the
 * recent-entries half
 * ({@see AuditEventRepository::findPaginated()} with no filter
 * and `per_page = $recentLimit`) into a single
 * {@see GetAuditEventsSummaryResult} with the recent slice
 * already projected through the §12.4 per-row helper
 * {@see ListAuditEventsUseCase::projectRow()}.
 *
 * Default-fill rule:
 *
 *   - The histogram half from
 *     {@see AuditEventRepository::countByAuditKind()} omits kinds
 *     with zero occurrences. The use case default-fills the
 *     returned map against
 *     {@see AuditEventListQuery::VALID_AUDIT_KINDS} so REST
 *     consumers can render a stable histogram (every known kind
 *     present, zero-occurrence kinds carrying `0`) without
 *     having to know which kinds the persistence layer happened
 *     to omit.
 *
 * Recent-limit clamping:
 *
 *   - The `recent_limit` axis is bounded inclusively to
 *     `[1, MAX_RECENT_LIMIT]` at the use-case boundary. Out-of-
 *     range inputs raise {@see \InvalidArgumentException} so the
 *     REST controller can route the failure through the §4.4
 *     validation error envelope.
 *
 *   - The maximum is deliberately lower than
 *     {@see AuditEventListQuery::MAX_PER_PAGE} (100) because the
 *     summary surface is intended as a "summary card" entry point,
 *     not a substitute for the §12.4 paginated list. Operators
 *     that need more rows should follow the entry-point link to
 *     `GET /clb/v1/audit/events`.
 *
 * Total-row reporting:
 *
 *   - The `total` axis is computed as the sum of the histogram
 *     map's values rather than a separate
 *     {@see AuditEventRepository::count()} round-trip. The two
 *     are equivalent (both consult the same underlying table),
 *     and folding the sum here keeps the summary surface to a
 *     single repository round-trip per axis.
 *
 * The §10.2 `safe_summary` slot on each projected `recent` row is
 * contractually scrubbed by the audit writer (typically through
 * the same {@see \CommerceLinkBuilder\Application\Logs\LogMasker}
 * family that scrubs `clb_logs`), so this use case does NOT
 * re-mask the field — re-masking would risk double-encoding
 * already-redacted tokens and would shadow the writer's
 * responsibility for keeping the audit trail tamper-evident.
 *
 * Mirrors the {@see \CommerceLinkBuilder\Application\Logs\UseCase\GetLogsSummaryUseCase}
 * shape so the §12.5 audit summary path reuses the same
 * orchestration conventions the §12.3 logs summary surface
 * already established.
 *
 * @package CommerceLinkBuilder\Application\Audit\UseCase
 */

declare(strict_types=1);

namespace CommerceLinkBuilder\Application\Audit\UseCase;

use CommerceLinkBuilder\Infrastructure\Persistence\AuditEventListQuery;
use CommerceLinkBuilder\Infrastructure\Persistence\AuditEventRepository;
use InvalidArgumentException;

/**
 * Application-layer use case for `GET /clb/v1/audit/events/summary` (§12.5).
 *
 * See the file-level docblock for the orchestration shape
 * (histogram + recent slice → default-fill → projected result),
 * the recent-limit clamping rule, and the rationale for keeping
 * `total` as a folded sum of the histogram values rather than a
 * separate `count()` round-trip.
 */
final class GetAuditEventsSummaryUseCase {

	/**
	 * Default recent-entries limit when the caller does not supply
	 * one. Matches the §12.5 "summary card" expected size and
	 * mirrors {@see \CommerceLinkBuilder\Application\Logs\UseCase\GetLogsSummaryUseCase::DEFAULT_RECENT_LIMIT}.
	 */
	public const DEFAULT_RECENT_LIMIT = 10;

	/**
	 * Maximum recent-entries limit accepted by {@see self::execute()}.
	 *
	 * Capped well below {@see AuditEventListQuery::MAX_PER_PAGE}
	 * (100) because the summary surface is intended as a "summary
	 * card" entry point, not a substitute for the §12.4 paginated
	 * list. Operators that need more rows follow the entry-point
	 * link to `GET /clb/v1/audit/events`.
	 */
	public const MAX_RECENT_LIMIT = 50;

	/**
	 * Persistence-layer query surface.
	 */
	private AuditEventRepository $events;

	/**
	 * Construct the use case with explicit collaborator
	 * injection.
	 *
	 * @param AuditEventRepository $events Persistence-layer query surface.
	 */
	public function __construct( AuditEventRepository $events ) {
		$this->events = $events;
	}

	/**
	 * Execute the §12.5 audit-events-summary query.
	 *
	 * Calls {@see AuditEventRepository::countByAuditKind()} for
	 * the per-kind histogram, then
	 * {@see AuditEventRepository::findPaginated()} with no
	 * filter and `per_page = $recentLimit, page = 1` for the
	 * newest-first recent slice. Each returned record is
	 * projected through the §12.4 per-row helper
	 * {@see ListAuditEventsUseCase::projectRow()} so the summary
	 * row shape exactly matches the list row shape.
	 *
	 * The histogram is default-filled against
	 * {@see AuditEventListQuery::VALID_AUDIT_KINDS} before being
	 * handed to the result VO so REST consumers always observe a
	 * stable map shape regardless of which kinds the persistence
	 * layer happened to omit. The `total` axis is computed as the
	 * sum of the histogram values to keep the summary surface to
	 * a single round-trip per axis.
	 *
	 * @param int $recentLimit Recent-entries limit (1 .. {@see self::MAX_RECENT_LIMIT}).
	 *
	 * @throws InvalidArgumentException When `$recentLimit` is out
	 *                                  of the [1, MAX_RECENT_LIMIT]
	 *                                  range.
	 */
	public function execute( int $recentLimit = self::DEFAULT_RECENT_LIMIT ): GetAuditEventsSummaryResult {
		if ( $recentLimit < 1 || $recentLimit > self::MAX_RECENT_LIMIT ) {
			$message = sprintf(
				'GetAuditEventsSummaryUseCase: recentLimit must be 1..%d; got %d.',
				self::MAX_RECENT_LIMIT,
				$recentLimit
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message; not user output.
			throw new InvalidArgumentException( $message );
		}

		$rawCounts = $this->events->countByAuditKind();
		$byKind    = self::defaultFillCounts( $rawCounts );

		$page = $this->events->findPaginated(
			new AuditEventListQuery(
				null,
				null,
				null,
				null,
				1,
				$recentLimit
			)
		);

		$recent = array();
		foreach ( $page->items() as $record ) {
			$recent[] = ListAuditEventsUseCase::projectRow( $record );
		}

		$total = array_sum( $byKind );

		return new GetAuditEventsSummaryResult(
			$byKind,
			$recent,
			$recentLimit,
			$total
		);
	}

	/**
	 * Default-fill the supplied per-kind count map against
	 * {@see AuditEventListQuery::VALID_AUDIT_KINDS} so every
	 * known kind has a numeric entry.
	 *
	 * Kinds the persistence layer omitted (zero occurrences)
	 * carry `0` in the returned map; kinds the persistence layer
	 * reported are preserved verbatim. Unknown kinds in
	 * `$rawCounts` (defensive: should never happen because the
	 * persistence layer enforces the closed vocabulary) are
	 * passed through so an operator inspecting the histogram can
	 * still see anomalous data rather than have it silently
	 * dropped.
	 *
	 * @param array<string, int> $rawCounts Per-kind counts as
	 *                                      reported by
	 *                                      {@see AuditEventRepository::countByAuditKind()}.
	 *
	 * @return array<string, int>
	 */
	private static function defaultFillCounts( array $rawCounts ): array {
		$filled = array();
		foreach ( AuditEventListQuery::VALID_AUDIT_KINDS as $kind ) {
			$filled[ $kind ] = $rawCounts[ $kind ] ?? 0;
		}

		// Preserve any unexpected kinds the persistence layer
		// surfaced so an anomaly does not get silently dropped.
		foreach ( $rawCounts as $kind => $count ) {
			if ( ! array_key_exists( $kind, $filled ) ) {
				$filled[ $kind ] = $count;
			}
		}

		return $filled;
	}
}
