<?php

declare(strict_types=1);

namespace App\Shared\Infrastructure\Audit;

use App\Shared\Application\Port\AuditLogger as AuditLoggerInterface;
use App\Shared\Domain\Clock;
use App\Shared\Infrastructure\Tenant\TenantContext;
use Doctrine\DBAL\Connection;
use Override;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

/**
 * Centralized audit logging service.
 *
 * All sensitive actions are logged to the append-only audit_log table.
 * Metadata (tenant, user, IP, correlation_id) is automatically injected.
 *
 * IMPORTANT: No PII in logs (NFR-S3). IPs/emails are hashed.
 *
 * @see Story 1.7 - T2: AuditLogger Service
 * @see FR90: Tracage actions sensibles
 * @see NFR-S3: Pas de PII dans les logs
 * @see NFR-S7: Audit trail immutable
 */
final readonly class AuditLogger implements AuditLoggerInterface
{
    public function __construct(
        private Connection $connection,
        private TenantContext $tenantContext,
        private TokenStorageInterface $tokenStorage,
        private RequestStack $requestStack,
        private Clock $clock,
        private string $appSecret,
    ) {
    }

    #[Override]
    public function logAuthentication(
        string $eventType,
        ?UuidInterface $userId,
        array $payload,
        ?string $tenantId = null,
    ): void {
        $this->log(
            aggregateType: 'User',
            aggregateId: $userId,
            eventType: $eventType,
            payload: $payload,
            tenantIdOverride: $tenantId,
            userIdOverride: $userId?->toString(),
        );
    }

    #[Override]
    public function logDataChange(
        string $aggregateType,
        UuidInterface $aggregateId,
        string $eventType,
        array $oldValues,
        array $newValues,
        ?string $reason = null,
    ): void {
        $payload = [
            'old_values' => $oldValues,
            'new_values' => $newValues,
        ];

        if ($reason !== null) {
            $payload['reason'] = $reason;
        }

        $this->log(
            aggregateType: $aggregateType,
            aggregateId: $aggregateId,
            eventType: $eventType,
            payload: $payload,
        );
    }

    #[Override]
    public function logExport(
        string $exportType,
        int $recordCount,
        ?string $targetDescription = null,
    ): void {
        $payload = [
            'export_type' => $exportType,
            'record_count' => $recordCount,
        ];

        if ($targetDescription !== null) {
            $payload['target'] = $targetDescription;
        }

        $this->log(
            aggregateType: 'Export',
            aggregateId: null,
            eventType: 'ExportGenerated',
            payload: $payload,
        );
    }

    #[Override]
    public function logAccess(
        string $resourceType,
        UuidInterface $resourceId,
        array $context = [],
    ): void {
        $this->log(
            aggregateType: $resourceType,
            aggregateId: $resourceId,
            eventType: 'ResourceAccessed',
            payload: $context,
        );
    }

    /**
     * Core logging method - writes to append-only audit_log table.
     *
     * @param array<string, mixed> $payload          Event-specific data
     * @param string|null          $tenantIdOverride Override tenant (for async handlers without TenantContext)
     * @param string|null          $userIdOverride   Override user (for async handlers without security token)
     */
    private function log(
        string $aggregateType,
        ?UuidInterface $aggregateId,
        string $eventType,
        array $payload,
        ?string $tenantIdOverride = null,
        ?string $userIdOverride = null,
    ): void {
        $metadata = $this->buildMetadata($tenantIdOverride, $userIdOverride);

        // Pass raw arrays - DBAL handles JSON encoding with 'json' type binding
        $this->connection->insert('audit_log', [
            'aggregate_type' => $aggregateType,
            'aggregate_id' => $aggregateId?->toString(),
            'event_type' => $eventType,
            'payload' => $payload,
            'metadata' => $metadata,
            'occurred_at' => $this->clock->now()->format('Y-m-d H:i:s.uP'),
        ], [
            'payload' => 'json',
            'metadata' => 'json',
        ]);
    }

    /**
     * Build metadata with automatic context injection.
     *
     * @param string|null $tenantIdOverride Override tenant from event (for async handlers)
     * @param string|null $userIdOverride   Override user from event (for async handlers)
     *
     * @return array<string, string|null>
     */
    private function buildMetadata(?string $tenantIdOverride = null, ?string $userIdOverride = null): array
    {
        $request = $this->requestStack->getCurrentRequest();

        $metadata = [
            'correlation_id' => CorrelationIdHolder::getOrGenerate()->value(),
            'occurred_at' => $this->clock->now()->format('c'),
        ];

        // Tenant ID: use override if provided, otherwise try TenantContext
        // Override is needed for async handlers that don't have request context
        if ($tenantIdOverride !== null) {
            $metadata['tenant_id'] = $tenantIdOverride;
        } elseif ($this->tenantContext->hasTenant()) {
            $metadata['tenant_id'] = (string) $this->tenantContext->getCurrentTenantId();
        }

        // User ID: use override if provided, otherwise try security token
        // Override is needed for async handlers that don't have security context
        if ($userIdOverride !== null) {
            $metadata['user_id'] = $userIdOverride;
        } else {
            $token = $this->tokenStorage->getToken();
            if ($token !== null) {
                $user = $token->getUser();
                if ($user !== null && method_exists($user, 'userId')) {
                    $metadata['user_id'] = $user->userId();
                }
            }
        }

        // IP and User-Agent (hashed for privacy)
        if ($request !== null) {
            $ip = $request->getClientIp();
            if ($ip !== null) {
                $metadata['ip_hash'] = $this->hashValue($ip);
            }

            $userAgent = $request->headers->get('User-Agent');
            if ($userAgent !== null) {
                $metadata['user_agent_hash'] = hash('sha256', $userAgent);
            }
        }

        return $metadata;
    }

    /**
     * Hash sensitive values (IP, email) using app secret.
     * Allows correlation without storing PII.
     */
    private function hashValue(string $value): string
    {
        return hash('sha256', $value . $this->appSecret);
    }
}
