<?php

declare(strict_types=1);

namespace App\Module\Core\Infrastructure\Audit;

use DateTimeImmutable;
use DateTimeZone;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Types\Types;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Uid\Uuid;

/**
 * Low-level service responsible for writing into the `audit_log` table.
 *
 * Uses a dedicated `audit` DBAL connection (same DSN as `default`) to write
 * outside the ORM transaction: audit rows survive an application-side
 * rollback and avoid transactional entanglement in batch (fixtures).
 *
 * Sensitive keys are stripped in defense-in-depth even when entities already
 * declare those properties #[AuditIgnore]. SQL failures are swallowed by the
 * caller (AuditListener wraps log() in try/catch) — audit must never crash a
 * business flow.
 */
final class AuditLogWriter
{
    /** @var list<string> keys always stripped from the `changes` payload */
    private const array SENSITIVE_KEYS = ['password', 'plainPassword', 'apiToken', 'token', 'secret'];

    public function __construct(
        #[Autowire(service: 'doctrine.dbal.audit_connection')]
        private readonly Connection $connection,
        private readonly Security $security,
        private readonly RequestStack $requestStack,
        private readonly RequestIdProvider $requestIdProvider,
    ) {}

    /**
     * @param string               $entityType Format "module.Entity" (e.g. "core.User")
     * @param string               $entityId   Entity id (int or serialized UUID)
     * @param string               $action     create|update|delete
     * @param array<string, mixed> $changes    JSON payload (sensitive keys stripped)
     */
    public function log(
        string $entityType,
        string $entityId,
        string $action,
        array $changes,
    ): void {
        $filteredChanges = $this->stripSensitive($changes);

        $this->connection->insert('audit_log', [
            'id'           => Uuid::v7()->toRfc4122(),
            'entity_type'  => $entityType,
            'entity_id'    => $entityId,
            'action'       => $action,
            'changes'      => $filteredChanges,
            'performed_by' => $this->security->getUser()?->getUserIdentifier() ?? 'system',
            'performed_at' => new DateTimeImmutable('now', new DateTimeZone('UTC')),
            'ip_address'   => $this->requestStack->getCurrentRequest()?->getClientIp(),
            'request_id'   => $this->requestIdProvider->getRequestId(),
        ], [
            'id'           => Types::GUID,
            'changes'      => Types::JSON,
            'performed_at' => Types::DATETIMETZ_IMMUTABLE,
        ]);
    }

    /**
     * Recursively removes sensitive keys from the payload.
     *
     * @param array<string, mixed> $data
     *
     * @return array<string, mixed>
     */
    private function stripSensitive(array $data): array
    {
        foreach ($data as $key => $value) {
            if (in_array($key, self::SENSITIVE_KEYS, true)) {
                unset($data[$key]);

                continue;
            }
            if (is_array($value)) {
                $data[$key] = $this->stripSensitive($value);
            }
        }

        return $data;
    }
}
