<?php

declare(strict_types=1);

namespace App\Framework\Http\Middleware;

use App\Framework\Http\ETagManager;
use App\Framework\Http\Headers;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\HttpResponse;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\Next;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\ValueObjects\ETag;
use App\Framework\Logging\Logger;

/**
 * ETag middleware for HTTP caching support
 *
 * Handles ETag generation, If-None-Match/If-Match headers, and 304 Not Modified responses
 */
#[MiddlewarePriorityAttribute(MiddlewarePriority::LATE)]
final readonly class ETagMiddleware implements HttpMiddleware
{
    public function __construct(
        private ETagManager $etagManager,
        private ?Logger $logger = null,
        private array $excludePaths = ['/api/health', '/favicon.ico'],
        private array $excludeContentTypes = ['application/json', 'text/event-stream'],
        private bool $enabled = true
    ) {
    }

    public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
    {
        if (! $this->enabled || ! $this->etagManager->isEnabled()) {
            return $next($context);
        }

        $request = $context->request;

        // Skip for excluded paths
        $path = $request->path ?? '/';
        if ($this->isPathExcluded($path)) {
            return $next($context);
        }

        // Only handle GET and HEAD requests for caching
        $method = $request->method ?? 'GET';
        if (! in_array($method, ['GET', 'HEAD'], true)) {
            return $this->handleNonCacheableRequest($context, $next);
        }

        // Execute request
        $context = $next($context);
        $response = $context->response;

        if (! $response instanceof HttpResponse) {
            return $context;
        }

        // Skip for excluded content types
        if ($this->isContentTypeExcluded($response)) {
            return $context;
        }

        // Process ETag for the response
        return $this->processETagResponse($context, $request, $response);
    }

    /**
     * Handle non-cacheable requests (PUT, POST, DELETE, etc.)
     */
    private function handleNonCacheableRequest(MiddlewareContext $context, Next $next): MiddlewareContext
    {
        $request = $context->request;

        // For modification requests, we might want to validate preconditions
        $method = $request->method ?? 'GET';
        if (in_array($method, ['PUT', 'PATCH', 'DELETE'], true)) {

            // Check if there's an existing ETag we should validate against
            $ifMatch = $request->headers->get('If-Match');
            if (is_string($ifMatch) && ! empty($ifMatch)) {
                // Note: In a real implementation, you'd need to determine the current ETag
                // of the resource being modified. This would typically involve:
                // 1. Extracting resource ID from the request path
                // 2. Loading the resource and generating its current ETag
                // 3. Validating against the If-Match header
                //
                // For now, we'll continue with the request and let the controller handle it
                $this->logger?->debug('ETag precondition validation needed for modification request', [
                    'method' => $method,
                    'path' => $request->path ?? '/',
                    'if_match' => $ifMatch,
                ]);
            }
        }

        return $next($context);
    }

    /**
     * Process ETag for GET/HEAD responses
     */
    private function processETagResponse(MiddlewareContext $context, mixed $request, HttpResponse $response): MiddlewareContext
    {
        try {
            // Generate ETag from response content
            $content = $response->body ?? '';
            $etag = $this->etagManager->generateETag($content, [
                'content_type' => $response->headers->get('Content-Type', 'text/html'),
                'content_length' => strlen($content),
            ]);

            // Check for If-None-Match header
            if ($this->etagManager->checkIfNoneMatch($request, $etag)) {
                $this->logger?->info('ETag matched, returning 304 Not Modified', [
                    'etag' => $etag->toString(),
                    'path' => $request->path ?? '/',
                ]);

                // Create 304 response with additional headers
                $additionalHeaders = Headers::empty();

                // Preserve certain headers from the original response
                $preserveHeaders = ['Date', 'Last-Modified', 'Cache-Control', 'Vary'];
                foreach ($preserveHeaders as $headerName) {
                    $headerValue = $response->headers->get($headerName);
                    if (is_string($headerValue) && ! empty($headerValue)) {
                        $additionalHeaders = $additionalHeaders->with($headerName, $headerValue);
                    }
                }

                $notModifiedResponse = $this->etagManager->createNotModifiedResponse($etag, $additionalHeaders);

                return $context->withResponse($notModifiedResponse);
            }

            // Add ETag to successful response
            $responseWithETag = $this->etagManager->addETagToResponse($response, $etag);

            $this->logger?->debug('ETag added to response', [
                'etag' => $etag->toString(),
                'path' => $request->path ?? '/',
                'content_length' => strlen($content),
                'is_weak' => $etag->isWeak(),
            ]);

            return $context->withResponse($responseWithETag);

        } catch (\Exception $e) {
            $this->logger?->warning('ETag processing failed', [
                'error' => $e->getMessage(),
                'path' => $request->path ?? '/',
            ]);

            // Return original response on error
            return $context;
        }
    }

    /**
     * Check if path should be excluded from ETag processing
     */
    private function isPathExcluded(string $path): bool
    {
        foreach ($this->excludePaths as $excludePath) {
            if (str_starts_with($path, $excludePath)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check if content type should be excluded from ETag processing
     */
    private function isContentTypeExcluded(HttpResponse $response): bool
    {
        $contentType = $response->headers->get('Content-Type', '');
        if (! is_string($contentType)) {
            return false;
        }

        foreach ($this->excludeContentTypes as $excludeType) {
            if (str_contains($contentType, $excludeType)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Create middleware with custom configuration
     */
    public static function create(
        ETagManager $etagManager,
        ?Logger $logger = null,
        array $options = []
    ): self {
        return new self(
            etagManager: $etagManager,
            logger: $logger,
            excludePaths: $options['exclude_paths'] ?? ['/api/health', '/favicon.ico'],
            excludeContentTypes: $options['exclude_content_types'] ?? ['application/json', 'text/event-stream'],
            enabled: $options['enabled'] ?? true
        );
    }
}
