<?php
 
namespace App\EventSubscriber;
 
use App\Entity\LogsAction;
 
use App\Entity\User;
 
use App\Repository\LogsActionRepository;
 
use DateTime;
 
use DateTimeInterface;
 
use Doctrine\Common\Collections\Collection;
 
use Doctrine\ORM\Event\LifecycleEventArgs;
 
use Doctrine\Persistence\ManagerRegistry;
 
use Doctrine\Persistence\ObjectManager;
 
use Error;
 
use Exception;
 
use ReflectionMethod;
 
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
use Symfony\Component\HttpFoundation\File\UploadedFile;
 
use Symfony\Component\HttpFoundation\Request;
 
use Symfony\Component\HttpKernel\Event\ControllerEvent;
 
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
 
use Symfony\Component\HttpKernel\Event\ResponseEvent;
 
use Symfony\Component\HttpKernel\KernelEvents;
 
use Symfony\Component\Security\Core\Security;
 
 
class AuditSubscriber implements EventSubscriberInterface
 
{
 
    private array $httpRequestContext = [];
 
    private array $entityChanges = [];
 
    private bool $shouldSkipAudit = false;
 
    private ?array $errorData = null;
 
    private ?array $preDeleteEntityData = null;
 
    private ?string $currentRoute = null;
 
    private ?string $currentMethod = null;
 
 
    // Routes to ignore from audit logging
 
    private const IGNORED_ROUTES = [
 
        // Clean route patterns
 
        '/api/discussion/list',
 
        '/api/login_check',
 
        // Original Symfony route patterns  
 
        '_api_/discussion/list',
 
        'api/login_check',
 
        '_api_/discussion/list.{_format}_get',
 
        'api/login_check_post',
 
    ];
 
 
    public function __construct(
 
        private readonly \Doctrine\ORM\EntityManagerInterface $entityManager,
 
        private readonly ManagerRegistry $doctrine,
 
        private Security $security,
 
        private readonly LogsActionRepository $logsActionRepository
 
    ) {
 
    }
 
 
    public static function getSubscribedEvents() :array
 
    {
 
        return [
 
            // HTTP Kernel Events
 
            KernelEvents::CONTROLLER => 'onKernelController',
 
            KernelEvents::RESPONSE => 'onKernelResponse',
 
            KernelEvents::EXCEPTION => 'onKernelException',
 
        ];
 
    }
 
 
    public function getSubscribedDoctrineEvents(): array
 
    {
 
        return [
 
            'prePersist',
 
            'preUpdate',
 
            'preRemove',
 
        ];
 
    }
 
 
    // Spammed routes like /api/login_check or /api/discussion/list
 
    private function shouldIgnoreRoute(string $originalRoute = null, string $cleanRoute = null): bool
 
    {
 
        if (!$originalRoute && !$cleanRoute) {
 
            return false;
 
        }
 
        if ($originalRoute && in_array($originalRoute, self::IGNORED_ROUTES, true)) {
 
            return true;
 
        }
 
        if ($cleanRoute && in_array($cleanRoute, self::IGNORED_ROUTES, true)) {
 
            return true;
 
        }
 
        foreach (self::IGNORED_ROUTES as $ignoredRoute) {
 
            if ($originalRoute && str_starts_with($originalRoute, $ignoredRoute)) {
 
                return true;
 
            }
 
            if ($originalRoute && $this->matchesRoutePattern($originalRoute, $ignoredRoute)) {
 
                return true;
 
            }
 
        }
 
        return false;
 
    }
 
    private function matchesRoutePattern(string $route, string $pattern): bool
 
    {
 
        // Remove format and method suffixes for comparison
 
        $cleanedRoute = preg_replace('/\.\{_format\}(_\w+)?$/', '', $route);
 
        $cleanedRoute = preg_replace('/_(?:get|post|put|patch|delete)$/', '', $cleanedRoute);
 
        
 
        $cleanedPattern = preg_replace('/\.\{_format\}(_\w+)?$/', '', $pattern);
 
        $cleanedPattern = preg_replace('/_(?:get|post|put|patch|delete)$/', '', $cleanedPattern);
 
        
 
        return $cleanedRoute === $cleanedPattern;
 
    }
 
 
    private function extractEntityData($entity): array
 
    {
 
        try {
 
            $data = [];
 
            $reflection = new \ReflectionClass($entity);
 
 
            // Always include entity class for clarity
 
            $data['_entity_class'] = get_class($entity);
 
 
            // Try to get entity ID
 
            if (method_exists($entity, 'getId')) {
 
                try {
 
                    $id = $entity->getId();
 
                    $data['_entity_id'] = $id ?? 'null';
 
                } catch (Error $e) {
 
                    $data['_entity_id'] = 'uninitialized';
 
                }
 
            }
 
 
            foreach ($reflection->getProperties() as $property) {
 
                $propertyName = $property->getName();
 
 
                try {
 
                    $value = $property->getValue($entity);
 
                } catch (Error $e) {
 
                    // Skip uninitialized properties
 
                    if (str_contains($e->getMessage(), 'must not be accessed before initialization')) {
 
                        continue;
 
                    }
 
                    $data[$propertyName] = 'error:' . $e->getMessage();
 
                    continue;
 
                }
 
 
                // Handle different value types
 
                if (is_null($value)) {
 
                    $data[$propertyName] = null;
 
                } elseif ($value instanceof DateTimeInterface) {
 
                    $data[$propertyName] = $value->format('Y-m-d H:i:s');
 
                } elseif (is_scalar($value)) {
 
                    // Truncate long strings for readability
 
                    if (is_string($value) && strlen($value) > 500) {
 
                        $data[$propertyName] = substr($value, 0, 500) . '... [truncated]';
 
                    } else {
 
                        $data[$propertyName] = $value;
 
                    }
 
                } elseif (is_object($value)) {
 
                    if (method_exists($value, 'getId')) {
 
                        // Related entity - store only ID
 
                        try {
 
                            $relatedId = $value->getId();
 
                            $data[$propertyName] = [
 
                                '_type' => 'relation',
 
                                '_class' => get_class($value),
 
                                'id' => $relatedId
 
                            ];
 
                        } catch (Error $e) {
 
                            $data[$propertyName] = [
 
                                '_type' => 'relation',
 
                                '_class' => get_class($value),
 
                                'id' => 'uninitialized'
 
                            ];
 
                        }
 
                    } elseif ($value instanceof Collection || $value instanceof \Doctrine\ORM\PersistentCollection) {
 
                        // Collection - check if initialized
 
                        try {
 
                            if (method_exists($value, 'isInitialized') && !$value->isInitialized()) {
 
                                $data[$propertyName] = ['_type' => 'collection', 'status' => 'uninitialized'];
 
                            } else {
 
                                $data[$propertyName] = ['_type' => 'collection', 'count' => $value->count()];
 
                            }
 
                        } catch (Exception $e) {
 
                            $data[$propertyName] = ['_type' => 'collection', 'status' => 'error'];
 
                        }
 
                    } elseif (method_exists($value, '__toString')) {
 
                        try {
 
                            $data[$propertyName] = (string)$value;
 
                        } catch (Exception $e) {
 
                            $data[$propertyName] = 'object:' . get_class($value);
 
                        }
 
                    } else {
 
                        $data[$propertyName] = 'object:' . get_class($value);
 
                    }
 
                } elseif (is_array($value)) {
 
                    $data[$propertyName] = ['_type' => 'array', 'count' => count($value)];
 
                }
 
            }
 
 
            return $data;
 
 
        } catch (Exception $e) {
 
            return [
 
                '_error' => 'extraction_failed',
 
                '_entity_class' => get_class($entity),
 
                '_message' => $e->getMessage()
 
            ];
 
        }
 
    }
 
 
    // This function is useless right now but in case we need to specify which HTTP actions to log later
 
    private function shouldLogHttpAction(string $method, int $statusCode): bool
 
    {
 
        // Log all successful requests for certain methods
 
        if ($statusCode >= 200 && $statusCode < 300) {
 
            return in_array($method, ['GET', 'DELETE', 'POST', 'PUT', 'PATCH']);
 
        }
 
        // Log all error responses
 
        return $statusCode >= 400;
 
    }
 
 
 
    private function getHttpActionType(string $method, string $route = null): string
 
    {
 
        if ($route && $this->isDeleteRoute($route)) {
 
            return 'delete';
 
        }
 
        
 
        return match ($method) {
 
            'GET' => 'read',
 
            'POST' => 'create',
 
            'PUT', 'PATCH' => 'update', 
 
            'DELETE' => 'delete',
 
            default => 'request'
 
        };
 
    }
 
 
    // Some Delete routes are using POST method so u need to handle them separately
 
    private function isDeleteRoute(string $route): bool
 
    {
 
        $deletePatterns = [
 
            '/delete',
 
            'delete_',
 
            '_delete',
 
            '/remove',
 
            'remove_',
 
            '_remove',
 
            '/destroy',
 
            'destroy_',
 
            '_destroy'
 
        ];
 
        
 
        $lowerRoute = strtolower($route);
 
        
 
        foreach ($deletePatterns as $pattern) {
 
            if (str_contains($lowerRoute, $pattern)) {
 
                return true;
 
            }
 
        }
 
        
 
        return false;
 
    }
 
 
    private function captureEntityBeforeDelete(Request $request, string $route): void
 
    {
 
        try {
 
            $entityId = $this->extractEntityIdFromRequest($request);
 
            if (!$entityId) {
 
                return;
 
            }
 
            $entityClass = $this->getEntityFromRoute($route);
 
            if (!$entityClass) {
 
                return;
 
            }
 
 
            $repository = $this->entityManager->getRepository($entityClass);
 
            $entity = $repository->find($entityId);
 
            
 
            if ($entity) {
 
                $this->preDeleteEntityData = $this->extractEntityData($entity);
 
            }
 
        } catch (Exception $e) {
 
        }
 
    }
 
 
    private function extractEntityIdFromRequest(Request $request): ?int
 
    {
 
        $idParams = ['id', 'entityId', 'siteId', 'userId', 'customerId'];
 
        
 
        foreach ($idParams as $param) {
 
            $value = $request->get($param);
 
            if ($value && is_numeric($value)) {
 
                return (int) $value;
 
            }
 
        }
 
        $pathInfo = $request->getPathInfo();
 
        if (preg_match('/\/(\d+)(?:\/|$)/', $pathInfo, $matches)) {
 
            $id = (int) $matches[1];
 
            return $id;
 
        }
 
        return null;
 
    }
 
 
    private function getEntityFromRoute(string $route): ?string
 
    {
 
        $routeToEntityMap = [
 
            'sites' => 'App\Entity\Site',
 
            'users' => 'App\Entity\User', 
 
            'customers' => 'App\Entity\Customer',
 
            'evaluations' => 'App\Entity\Evaluation',
 
            'documents' => 'App\Entity\Document',
 
            'categories' => 'App\Entity\Category',
 
            'sections' => 'App\Entity\Section',
 
            'compliances' => 'App\Entity\Compliance',
 
            //
 
            'site' => 'App\Entity\Site',
 
            'user' => 'App\Entity\User',
 
            'customer' => 'App\Entity\Customer', 
 
            'evaluation' => 'App\Entity\Evaluation',
 
            'document' => 'App\Entity\Document',
 
            'category' => 'App\Entity\Category',
 
            'section' => 'App\Entity\Section',
 
            'compliance' => 'App\Entity\Compliance',
 
        ];
 
 
        $lowerRoute = strtolower($route);
 
        
 
        foreach ($routeToEntityMap as $routePattern => $entityClass) {
 
            if (str_contains($lowerRoute, $routePattern)) {
 
                return $entityClass;
 
            }
 
        }
 
 
        return null;
 
    }
 
 
 
    private function getAuditEntityManager(): ObjectManager
 
    {
 
        try {
 
            $em = $this->doctrine->resetManager();
 
            return $this->doctrine->getManager();
 
        } catch (Exception $e) {
 
            return $this->entityManager;
 
        }
 
    }
 
 
    private function getCleanRoute($request): string
 
    {
 
        // Try to get the actual path first
 
        $pathInfo = $request->getPathInfo();
 
        if ($pathInfo && $pathInfo !== '/') {
 
            return $pathInfo;
 
        }
 
        
 
        // Fallback to route name, but clean it up
 
        $route = $request->attributes->get('_route');
 
        if (!$route) {
 
            return 'unknown_route';
 
        }
 
        
 
        // Clean up common Symfony route patterns
 
        $cleanRoute = $route;
 
        
 
        // Remove format placeholders
 
        $cleanRoute = preg_replace('/\.\{_format\}/', '', $cleanRoute);
 
        
 
        // Remove method suffixes (like _post, _get, etc.)
 
        $cleanRoute = preg_replace('/_(?:get|post|put|patch|delete)$/', '', $cleanRoute);
 
        
 
        // Convert underscores to forward slashes for API routes
 
        if (str_starts_with($cleanRoute, '_api_')) {
 
            $cleanRoute = str_replace('_api_', '/api/', $cleanRoute);
 
            $cleanRoute = str_replace('_', '/', $cleanRoute);
 
        }
 
        
 
        return $cleanRoute;
 
    }
 
 
    public function onKernelController(ControllerEvent $event)
 
    {
 
        $request = $event->getRequest();
 
        $originalRoute = $request->attributes->get('_route');
 
        $cleanRoute = $this->getCleanRoute($request);
 
 
        if ($this->shouldIgnoreRoute($originalRoute, $cleanRoute)) {
 
            $this->shouldSkipAudit = true;
 
            return;
 
        }
 
 
        $this->shouldSkipAudit = false;
 
        $this->currentRoute = $cleanRoute;
 
        $this->currentMethod = $request->getMethod();
 
 
        // Build structured request data
 
        $requestPayload = [];
 
 
        // Query parameters
 
        if ($request->query->count() > 0) {
 
            $requestPayload['query_params'] = $request->query->all();
 
        }
 
 
        // POST/Form data
 
        if ($request->request->count() > 0) {
 
            $requestPayload['form_data'] = $request->request->all();
 
        }
 
 
        // JSON body
 
        if ($request->getContentType() === 'json' || str_contains($request->headers->get('Content-Type', ''), 'application/json')) {
 
            $content = $request->getContent();
 
            if ($content) {
 
                $decodedContent = json_decode($content, true);
 
                if (json_last_error() === JSON_ERROR_NONE) {
 
                    $requestPayload['json_body'] = $decodedContent;
 
                } else {
 
                    $requestPayload['raw_body'] = substr($content, 0, 1000); // Limit size
 
                }
 
            }
 
        }
 
 
        // Files
 
        if ($request->files->count() > 0) {
 
            $requestPayload['files'] = array_map(function($file) {
 
                return $file instanceof UploadedFile
 
                    ? [
 
                        'name' => $file->getClientOriginalName(),
 
                        'size' => $file->getSize(),
 
                        'type' => $file->getMimeType()
 
                    ]
 
                    : (string)$file;
 
            }, $request->files->all());
 
        }
 
 
        // Store context for later use
 
        $this->httpRequestContext = [
 
            'route' => $cleanRoute,
 
            'original_route' => $originalRoute,
 
            'method' => $request->getMethod(),
 
            'ip_address' => $this->logsActionRepository->getRealIpAddress($request),
 
            'user_agent' => $request->headers->get('User-Agent'),
 
            'request_payload' => $requestPayload,
 
        ];
 
 
        // Capture entity data before potential deletion
 
        if ($this->isDeleteRoute($cleanRoute)) {
 
            $this->captureEntityBeforeDelete($request, $cleanRoute);
 
        }
 
    }
 
 
     public function preUpdate(LifecycleEventArgs $args)
 
     {
 
         if ($this->shouldSkipAudit) {
 
             return;
 
         }
 
 
         try {
 
             $entity = $args->getObject();
 
             $entityClass = get_class($entity);
 
             $entityId = $this->getEntityId($entity);
 
 
             // Get only the changed fields (before and after values)
 
             $dataBefore = $this->extractChangeSetData($entity);
 
             $dataAfter = $this->extractChangeSetDataAfter($entity);
 
 
             $this->entityChanges[] = [
 
                 'type' => 'entity_change',
 
                 'action' => 'update',
 
                 'entity_class' => $entityClass,
 
                 'entity_id' => $entityId,
 
                 'data_before' => $dataBefore, // Only modified fields - old values
 
                 'data_after' => $dataAfter,   // Only modified fields - new values
 
             ];
 
         } catch (Exception $e) {
 
             // Silently handle errors to prevent disrupting the update
 
         }
 
     }
 
 
    private function getEntityId($entity): string
 
    {
 
        if (!method_exists($entity, 'getId')) {
 
            return 'unknown';
 
        }
 
 
        try {
 
            $id = $entity->getId();
 
            return $id !== null ? (string)$id : 'null';
 
        } catch (Error $e) {
 
            if (str_contains($e->getMessage(), 'must not be accessed before initialization')) {
 
                return 'uninitialized';
 
            }
 
            return 'error';
 
        }
 
    }
 
 
    private function extractChangeSetData($entity): array
 
    {
 
        $dataBefore = [];
 
 
        try {
 
            $uow = $this->entityManager->getUnitOfWork();
 
            $changeSet = $uow->getEntityChangeSet($entity);
 
 
            // Only include fields that have actually changed
 
            foreach ($changeSet as $field => $values) {
 
                $oldValue = $values[0]; // Old value before change
 
 
                if ($oldValue instanceof DateTimeInterface) {
 
                    $dataBefore[$field] = $oldValue->format('Y-m-d H:i:s');
 
                } elseif (is_object($oldValue) && method_exists($oldValue, 'getId')) {
 
                    try {
 
                        $dataBefore[$field] = [
 
                            '_type' => 'relation',
 
                            '_class' => get_class($oldValue),
 
                            'id' => $oldValue->getId()
 
                        ];
 
                    } catch (Error $e) {
 
                        $dataBefore[$field] = [
 
                            '_type' => 'relation',
 
                            '_class' => get_class($oldValue),
 
                            'id' => 'uninitialized'
 
                        ];
 
                    }
 
                } elseif (is_scalar($oldValue) || is_null($oldValue)) {
 
                    $dataBefore[$field] = $oldValue;
 
                } else {
 
                    try {
 
                        $dataBefore[$field] = (string)$oldValue;
 
                    } catch (Exception $e) {
 
                        $dataBefore[$field] = 'object_conversion_error';
 
                    }
 
                }
 
            }
 
 
        } catch (Exception $e) {
 
            $dataBefore['_error'] = 'Could not retrieve change set: ' . $e->getMessage();
 
        }
 
 
        return $dataBefore;
 
    }
 
 
    private function extractChangeSetDataAfter($entity): array
 
    {
 
        $dataAfter = [];
 
 
        try {
 
            $uow = $this->entityManager->getUnitOfWork();
 
            $changeSet = $uow->getEntityChangeSet($entity);
 
 
            // Only include fields that have actually changed
 
            foreach ($changeSet as $field => $values) {
 
                $newValue = $values[1]; // New value after change
 
 
                if ($newValue instanceof DateTimeInterface) {
 
                    $dataAfter[$field] = $newValue->format('Y-m-d H:i:s');
 
                } elseif (is_object($newValue) && method_exists($newValue, 'getId')) {
 
                    try {
 
                        $dataAfter[$field] = [
 
                            '_type' => 'relation',
 
                            '_class' => get_class($newValue),
 
                            'id' => $newValue->getId()
 
                        ];
 
                    } catch (Error $e) {
 
                        $dataAfter[$field] = [
 
                            '_type' => 'relation',
 
                            '_class' => get_class($newValue),
 
                            'id' => 'uninitialized'
 
                        ];
 
                    }
 
                } elseif (is_scalar($newValue) || is_null($newValue)) {
 
                    $dataAfter[$field] = $newValue;
 
                } else {
 
                    try {
 
                        $dataAfter[$field] = (string)$newValue;
 
                    } catch (Exception $e) {
 
                        $dataAfter[$field] = 'object_conversion_error';
 
                    }
 
                }
 
            }
 
 
        } catch (Exception $e) {
 
            $dataAfter['_error'] = 'Could not retrieve change set: ' . $e->getMessage();
 
        }
 
 
        return $dataAfter;
 
    }
 
 
 
    public function prePersist(LifecycleEventArgs $args)
 
    {
 
        if ($this->shouldSkipAudit) {
 
            return;
 
        }
 
 
        try {
 
            $entity = $args->getObject();
 
            $entityClass = get_class($entity);
 
            $entityId = $this->getEntityId($entity);
 
 
            $this->entityChanges[] = [
 
                'type' => 'entity_change',
 
                'action' => 'create',
 
                'entity_class' => $entityClass,
 
                'entity_id' => $entityId,
 
                'data_before' => [],
 
                'data_after' => $this->extractEntityData($entity),
 
            ];
 
        } catch (Exception $e) {
 
            // Silently handle errors
 
        }
 
    }
 
 
    // Capture entity deletions
 
    public function preRemove(LifecycleEventArgs $args)
 
    {
 
        if ($this->shouldSkipAudit) {
 
            return;
 
        }
 
 
        try {
 
            $entity = $args->getObject();
 
            $entityClass = get_class($entity);
 
            $entityId = $this->getEntityId($entity);
 
            $entityData = $this->extractEntityData($entity);
 
 
            $this->entityChanges[] = [
 
                'type' => 'entity_change',
 
                'action' => 'delete',
 
                'entity_class' => $entityClass,
 
                'entity_id' => $entityId,
 
                'data_before' => $entityData,
 
                'data_after' => [], // Empty after deletion
 
            ];
 
        } catch (Exception $e) {
 
            // Silently handle errors
 
        }
 
    }
 
    public function onKernelException(ExceptionEvent $event): void
 
    {
 
        if ($this->shouldSkipAudit) {
 
            return;
 
        }
 
 
        $exception = $event->getThrowable();
 
        $this->errorData = [
 
            'error_message' => $exception->getMessage(),
 
            'error_code' => $exception->getCode(),
 
            'error_file' => $exception->getFile(),
 
            'error_line' => $exception->getLine(),
 
            'error_trace' => $exception->getTraceAsString()
 
        ];
 
    }
 
 
    public function onKernelResponse(ResponseEvent $event): void
 
    {
 
        if ($this->shouldSkipAudit) {
 
            return;
 
        }
 
 
        $response = $event->getResponse();
 
        $user = $this->security->getUser();
 
        $statusCode = $response ? $response->getStatusCode() : 500;
 
        $method = $this->currentMethod ?? 'UNKNOWN';
 
        $route = $this->currentRoute ?? 'unknown_route';
 
 
        // Build clean response data
 
        $responseData = $this->buildResponseData($response, $statusCode);
 
 
        // Handle errors
 
        if ($statusCode >= 400 && empty($this->entityChanges)) {
 
            $this->logError($statusCode, $responseData);
 
        }
 
 
        // Log HTTP request if no entity changes were captured
 
        if (empty($this->entityChanges) && $this->shouldLogHttpAction($method, $statusCode)) {
 
            $this->logHttpRequest($method, $route, $statusCode, $responseData);
 
        }
 
 
        // Nothing to log
 
        if (empty($this->entityChanges)) {
 
            $this->cleanup();
 
            return;
 
        }
 
 
        // Persist all logs
 
        $this->persistLogs($user, $statusCode, $responseData);
 
 
        // Cleanup
 
        $this->cleanup();
 
    }
 
 
    private function buildResponseData($response, int $statusCode): array
 
    {
 
        $responseData = ['status_code' => $statusCode];
 
 
        if (!$response) {
 
            return $responseData;
 
        }
 
 
        // Important headers
 
        $responseData['headers'] = [];
 
        $importantHeaders = ['content-type', 'location', 'cache-control'];
 
        foreach ($importantHeaders as $header) {
 
            if ($response->headers->has($header)) {
 
                $responseData['headers'][$header] = $response->headers->get($header);
 
            }
 
        }
 
 
        // Response content (only for successful responses)
 
        if ($statusCode >= 200 && $statusCode < 300) {
 
            try {
 
                $content = $response->getContent();
 
                if (!empty($content)) {
 
                    // Limit response content size
 
                    if (strlen($content) > 5000) {
 
                        $responseData['content'] = substr($content, 0, 5000) . '... [truncated]';
 
                        $responseData['content_truncated'] = true;
 
                        $responseData['original_length'] = strlen($content);
 
                    } else {
 
                        $decodedContent = json_decode($content, true);
 
                        if (json_last_error() === JSON_ERROR_NONE) {
 
                            $responseData['content'] = $decodedContent;
 
                        } else {
 
                            $responseData['content'] = $content;
 
                        }
 
                    }
 
                }
 
            } catch (Exception $e) {
 
                $responseData['content_error'] = $e->getMessage();
 
            }
 
        }
 
 
        return $responseData;
 
    }
 
 
    private function logError(int $statusCode, array $responseData): void
 
    {
 
        $errorData = $this->errorData ?? [];
 
 
        $this->entityChanges[] = [
 
            'type' => 'error',
 
            'action' => 'error',
 
            'entity_class' => null,
 
            'entity_id' => null,
 
            'data_before' => [],
 
            'data_after' => array_merge($errorData, ['status_code' => $statusCode]),
 
            'response_data' => $responseData,
 
        ];
 
    }
 
 
    private function logHttpRequest(string $method, string $route, int $statusCode, array $responseData): void
 
    {
 
        $action = $this->getHttpActionType($method, $route);
 
        $dataBefore = [];
 
 
        // For DELETE operations, include pre-delete data
 
        if ($action === 'delete' && $this->preDeleteEntityData !== null) {
 
            $dataBefore = $this->preDeleteEntityData;
 
        }
 
 
        $this->entityChanges[] = [
 
            'type' => 'http_request',
 
            'action' => $action,
 
            'entity_class' => null,
 
            'entity_id' => null,
 
            'data_before' => $dataBefore,
 
            'data_after' => [],
 
            'response_data' => $responseData,
 
            'metadata' => [
 
                'detected_as_delete' => $action === 'delete' && $method !== 'DELETE',
 
                'has_pre_delete_data' => !empty($dataBefore),
 
            ],
 
        ];
 
    }
 
 
    private function persistLogs($user, int $statusCode, array $responseData): void
 
    {
 
        $auditEM = $this->getAuditEntityManager();
 
        if (!$auditEM) {
 
            return;
 
        }
 
 
        $auditUser = $this->resolveAuditUser($user, $auditEM);
 
        if (!$auditUser) {
 
            return; // Skip logging if no user can be resolved
 
        }
 
 
        foreach ($this->entityChanges as $change) {
 
            try {
 
                $log = new LogsAction();
 
                $log->setUser($auditUser);
 
                $log->setAction($change['action']);
 
                $log->setRoute($this->currentRoute);
 
                $log->setMethod($this->currentMethod);
 
                $log->setIpAddress($this->httpRequestContext['ip_address'] ?? null);
 
                $log->setUserAgent($this->httpRequestContext['user_agent'] ?? null);
 
                $log->setStatusCode($statusCode);
 
 
                // Build clear request data structure - always include request payload
 
                $requestData = [
 
                    'type' => $change['type'] ?? 'entity_change',
 
                    'entity_class' => $change['entity_class'] ?? null,
 
                    'entity_id' => $change['entity_id'] ?? null,
 
                    'payload' => $this->httpRequestContext['request_payload'] ?? [],
 
                    'metadata' => $change['metadata'] ?? [],
 
                ];
 
 
                // Set request data (always persisted, nullable)
 
                $log->setRequestData(!empty($requestData['payload']) || !empty($requestData['metadata'])
 
                    ? json_encode($requestData, JSON_UNESCAPED_UNICODE)
 
                    : null);
 
 
                // Set data before (only modified fields for updates, full snapshot for deletes)
 
                $dataBefore = $change['data_before'] ?? [];
 
                $log->setDataBefore(!empty($dataBefore) ? json_encode($dataBefore, JSON_UNESCAPED_UNICODE) : null);
 
 
                // Set data after (only modified fields for updates, full snapshot for creates)
 
                $dataAfter = $change['data_after'] ?? [];
 
                $log->setDataAfter(!empty($dataAfter) ? json_encode($dataAfter, JSON_UNESCAPED_UNICODE) : null);
 
 
                // Set response data (always persisted if available, nullable)
 
                $responseDataToLog = $change['response_data'] ?? $responseData;
 
                $log->setResponseData(!empty($responseDataToLog) ? json_encode($responseDataToLog, JSON_UNESCAPED_UNICODE) : null);
 
 
                $log->setCreatedAt(new DateTime());
 
                $log->setUpdatedAt(new DateTime());
 
 
                $auditEM->persist($log);
 
            } catch (Exception $e) {
 
                // Silently continue to next log
 
                continue;
 
            }
 
        }
 
 
        try {
 
            $auditEM->flush();
 
        } catch (Exception $e) {
 
            // Failed to persist logs - silent fail to avoid breaking the request
 
        }
 
    }
 
 
    private function resolveAuditUser($user, ObjectManager $auditEM): ?User
 
    {
 
        if (!$user) {
 
            $systemUser = $auditEM->find(User::class, 0);
 
            return $systemUser;
 
        }
 
 
        $auditUser = null;
 
 
        if ($user instanceof User) {
 
            $userId = $user->getId();
 
            $auditUser = $auditEM->find(User::class, $userId);
 
        }
 
 
        return $auditUser;
 
    }
 
 
    private function cleanup(): void
 
    {
 
        $this->entityChanges = [];
 
        $this->errorData = null;
 
        $this->httpRequestContext = [];
 
        $this->preDeleteEntityData = null;
 
        $this->currentRoute = null;
 
        $this->currentMethod = null;
 
    }
 
}