<?php

declare(strict_types=1);

namespace App\Infrastructure\Audit;

use App\Domain\Audit\AuditAction;
use App\Domain\Audit\AuditContext;
use App\Domain\Audit\AuditEmitter;
use Psr\Log\LoggerInterface;
use Throwable;

/**
 * Persists audit rows via {@see AuditRepository}. Two emit modes:
 *
 *  - {@see emit()}: best-effort. Failures are logged and swallowed so a
 *    DB hiccup on a high-volume public path doesn't 500 the user.
 *  - {@see emitOrThrow()}: strict. Failures propagate so an enclosing
 *    `Connection::transactional()` rolls back the originating mutation.
 *    SEC_REVIEW F4 — admin writes must not commit without an audit row.
 */
final class DbAuditEmitter implements AuditEmitter
{
    public function __construct(
        private readonly AuditRepository $repo,
        private readonly LoggerInterface $logger,
    ) {
    }

    public function emit(
        string $action,
        ?string $entityType,
        int|string|null $entityId,
        array $payload,
        AuditContext $context,
        ?string $entityLabel = null,
    ): void {
        $type = $entityType ?? AuditAction::entityTypeFor($action);

        try {
            $json = self::encodePayload($payload);
            $this->repo->insert($action, $type, $entityId, $entityLabel, $json, $context);
        } catch (Throwable $e) {
            $this->logger->error('audit_emit_failed', [
                'action' => $action,
                'entity_type' => $type,
                'entity_id' => $entityId,
                'entity_label' => $entityLabel,
                'error' => $e->getMessage(),
            ]);
        }
    }

    public function emitOrThrow(
        string $action,
        ?string $entityType,
        int|string|null $entityId,
        array $payload,
        AuditContext $context,
        ?string $entityLabel = null,
    ): void {
        $type = $entityType ?? AuditAction::entityTypeFor($action);
        $json = self::encodePayload($payload);
        $this->repo->insert($action, $type, $entityId, $entityLabel, $json, $context);
    }

    /**
     * @param array<string, mixed> $payload
     */
    private static function encodePayload(array $payload): string
    {
        return (string) json_encode(
            $payload,
            JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
        );
    }
}
