<?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;

/**
 * Service bas-niveau responsable de l'ecriture dans la table `audit_log`.
 *
 * Utilise une connexion DBAL dediee `audit` (meme DSN que `default`, service
 * separe) pour ecrire hors de la transaction ORM : indispensable pour que
 * les lignes d'audit survivent meme si le flush applicatif est rollback,
 * et pour eviter tout entanglement transactionnel en batch (fixtures).
 *
 * Les cles sensibles (password, plainPassword, token, secret) sont filtrees
 * en defense-in-depth meme si les entites declarent deja ces proprietes
 * #[AuditIgnore].
 *
 * Erreur silencieuse : en cas d'echec SQL, on lance pas l'exception plus
 * haut — l'audit ne doit jamais faire crasher un flux metier. Le listener
 * wrappe l'appel dans un try/catch + logger (cf. AuditListener).
 */
final class AuditLogWriter
{
    /** @var list<string> cles systematiquement strippees du payload `changes` */
    private const array SENSITIVE_KEYS = ['password', 'plainPassword', '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,
    ) {}

    /**
     * Ecrit une ligne d'audit.
     *
     * @param string               $entityType Format "module.Entity" (ex: "core.User")
     * @param string               $entityId   ID de l'entite (int ou UUID serialise)
     * @param string               $action     create|update|delete
     * @param array<string, mixed> $changes    Payload JSON (filtre des cles sensibles)
     */
    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(),
        ], [
            // Types de conversion DBAL : UUID natif PG + jsonb + datetimetz.
            // Sans 'id' => GUID, DBAL passerait un varchar et Postgres ferait
            // un cast implicite — ca marche mais l'intention est floue.
            'id'           => Types::GUID,
            'changes'      => Types::JSON,
            'performed_at' => Types::DATETIMETZ_IMMUTABLE,
        ]);
    }

    /**
     * Supprime recursivement les cles sensibles du payload.
     *
     * Utile pour les snapshots complets (create/delete) ou les changes
     * d'update : le listener prefiltre deja mais on garde cette garde
     * en defense-in-depth si un appelant direct oublie `#[AuditIgnore]`.
     *
     * Recursion : parcourt les sous-tableaux (ex: changes structures
     * `{field: {old, new}}`, snapshots avec relations imbriquees, ou
     * payload arbitraire pousse par un appelant direct).
     *
     * @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;
    }
}
