<?php

declare(strict_types=1);

namespace App\Framework\Telemetry\Middleware;

use App\Framework\Http\HttpMiddleware;
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\Telemetry\OperationHandle;
use App\Framework\Telemetry\UnifiedTelemetryService;

/**
 * Middleware for tracing HTTP requests
 */
#[MiddlewarePriorityAttribute(MiddlewarePriority::VERY_EARLY)]
final readonly class TelemetryHttpMiddleware implements HttpMiddleware
{
    /**
     * @param UnifiedTelemetryService $telemetryService Telemetry service
     */
    public function __construct(
        private UnifiedTelemetryService $telemetryService
    ) {
    }

    /**
     * Process the middleware context
     */
    public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
    {
        if (! $this->telemetryService->isEnabled()) {
            return $next($context);
        }

        $request = $context->request;

        // Start operation for the request
        $operation = $this->telemetryService->startOperation(
            $this->getSpanName($request),
            'http',
            $this->createRequestAttributes($request)
        );

        try {
            // Process the request through the middleware chain
            $resultContext = $next($context);

            // If we have a response, add response attributes
            if ($resultContext->hasResponse()) {
                $response = $resultContext->response;
                $this->addResponseAttributes($operation, $response);
                $this->setSpanStatus($operation, $response);
            }

            return $resultContext;
        } catch (\Throwable $e) {
            // Mark operation as failed
            $operation->fail($e->getMessage());

            // Re-throw the exception
            throw $e;
        } finally {
            // End the operation if it hasn't been ended yet
            if ($operation->getId() !== 'disabled' && $operation->getId() !== 'error') {
                $operation->end();
            }
        }
    }

    /**
     * Get the span name for the request
     */
    private function getSpanName($request): string
    {
        $method = $request->method;
        $path = $request->uri->getPath();

        // Use a generic name if the path is empty
        if (empty($path)) {
            $path = '/';
        }

        return "{$method} {$path}";
    }

    /**
     * Create attributes for the request
     *
     * @return array<string, mixed>
     */
    private function createRequestAttributes($request): array
    {
        $attributes = [
            'http.method' => $request->method,
            'http.url' => (string)$request->uri,
            'http.host' => $request->uri->getHost(),
            'http.scheme' => $request->uri->getScheme() ?: 'http',
            'http.target' => $request->uri->getPath(),
        ];

        // Add query parameters if present
        $query = $request->uri->getQuery();
        if (! empty($query)) {
            $attributes['http.query'] = $this->sanitizeQuery($query);
        }

        // Add content length if present
        $contentLength = $request->headers->get('Content-Length');
        if (! empty($contentLength)) {
            $attributes['http.request_content_length'] = (int)$contentLength;
        }

        // Add user agent if present
        $userAgent = $request->headers->get('User-Agent');
        if (! empty($userAgent)) {
            $attributes['http.user_agent'] = $userAgent;
        }

        // Add client IP if available
        $clientIp = $this->getClientIp($request);
        if ($clientIp !== null) {
            $attributes['http.client_ip'] = $clientIp;
        }

        return $attributes;
    }

    /**
     * Add response attributes to the operation
     */
    private function addResponseAttributes(OperationHandle $operation, $response): void
    {
        $operation->addAttribute('http.status_code', $response->statusCode);

        // Add content length if present
        $contentLength = $response->headers->get('Content-Length');
        if (! empty($contentLength)) {
            $operation->addAttribute('http.response_content_length', (int)$contentLength);
        }
    }

    /**
     * Set the span status based on the response
     */
    private function setSpanStatus(OperationHandle $operation, $response): void
    {
        $statusCode = $response->statusCode;

        if ($statusCode >= 400) {
            $operation->end('error', "HTTP {$statusCode}");
        } else {
            $operation->end('success');
        }
    }

    /**
     * Sanitize query string to remove sensitive information
     */
    private function sanitizeQuery(string $query): string
    {
        parse_str($query, $params);

        // List of sensitive parameter names
        $sensitiveParams = ['password', 'token', 'api_key', 'apikey', 'secret', 'credential'];

        // Redact sensitive parameters
        foreach ($params as $key => $value) {
            if ($this->isSensitiveParameter($key, $sensitiveParams)) {
                $params[$key] = '[REDACTED]';
            }
        }

        return http_build_query($params);
    }

    /**
     * Check if a parameter name is sensitive
     *
     * @param string $paramName Parameter name
     * @param array<string> $sensitiveParams List of sensitive parameter names
     */
    private function isSensitiveParameter(string $paramName, array $sensitiveParams): bool
    {
        $paramName = strtolower($paramName);

        // Check for exact matches
        if (in_array($paramName, $sensitiveParams, true)) {
            return true;
        }

        // Check for partial matches
        foreach ($sensitiveParams as $sensitiveParam) {
            if (str_contains($paramName, $sensitiveParam)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Get the client IP address from the request
     */
    private function getClientIp($request): ?string
    {
        // Check X-Forwarded-For header
        $forwardedFor = $request->headers->get('X-Forwarded-For');
        if (! empty($forwardedFor)) {
            $ips = explode(',', $forwardedFor);

            return trim($ips[0]);
        }

        // Check REMOTE_ADDR server parameter
        $serverParams = $request->serverParams;
        if (isset($serverParams['REMOTE_ADDR'])) {
            return $serverParams['REMOTE_ADDR'];
        }

        return null;
    }
}
