<?php

declare(strict_types=1);

namespace App\Shared\Infrastructure\Middleware;

use App\Shared\Domain\CorrelationId;
use App\Shared\Infrastructure\Audit\CorrelationIdHolder;
use InvalidArgumentException;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
 * Generates or propagates correlation ID for request tracing.
 *
 * - If X-Correlation-Id header exists, uses it (distributed tracing)
 * - Otherwise generates a new UUID v7
 * - Stores in CorrelationIdHolder for access anywhere in the request
 * - Adds X-Correlation-Id header to response
 * - Clears on request termination (and defensively on exception)
 *
 * @see Story 1.7 - T3: Correlation ID
 */
final readonly class CorrelationIdMiddleware
{
    public const string HEADER_NAME = 'X-Correlation-Id';

    #[AsEventListener(event: KernelEvents::REQUEST, priority: 255)]
    public function onKernelRequest(RequestEvent $event): void
    {
        if (!$event->isMainRequest()) {
            return;
        }

        // Defensive cleanup: if previous request didn't terminate properly
        // (edge case: fatal error, script termination), clear stale ID
        CorrelationIdHolder::clear();

        $request = $event->getRequest();
        $existingId = $request->headers->get(self::HEADER_NAME);

        $correlationId = $this->parseOrGenerate($existingId);

        CorrelationIdHolder::set($correlationId);

        // Also store in request attributes for easy access
        $request->attributes->set('correlation_id', $correlationId);
    }

    #[AsEventListener(event: KernelEvents::RESPONSE, priority: -255)]
    public function onKernelResponse(ResponseEvent $event): void
    {
        if (!$event->isMainRequest()) {
            return;
        }

        $correlationId = CorrelationIdHolder::get();
        if ($correlationId !== null) {
            $event->getResponse()->headers->set(
                self::HEADER_NAME,
                $correlationId->value(),
            );
        }
    }

    /**
     * Store correlation ID in exception for error logging before cleanup.
     *
     * Priority -255: run after other exception handlers that might need the ID.
     */
    #[AsEventListener(event: KernelEvents::EXCEPTION, priority: -255)]
    public function onKernelException(ExceptionEvent $event): void
    {
        $correlationId = CorrelationIdHolder::get();
        if ($correlationId === null) {
            return;
        }

        // Add correlation ID to exception context if possible
        $exception = $event->getThrowable();
        if (method_exists($exception, 'setCorrelationId')) {
            $exception->setCorrelationId($correlationId->value());
        }
    }

    #[AsEventListener(event: KernelEvents::TERMINATE)]
    public function onKernelTerminate(TerminateEvent $event): void
    {
        CorrelationIdHolder::clear();
    }

    /**
     * Parse existing correlation ID or generate new one if invalid.
     *
     * We're lenient with malformed input - just generate a fresh ID.
     */
    private function parseOrGenerate(?string $existingId): CorrelationId
    {
        if ($existingId === null) {
            return CorrelationId::generate();
        }

        try {
            return CorrelationId::fromString($existingId);
        } catch (InvalidArgumentException) {
            return CorrelationId::generate();
        }
    }
}
