<?php

declare(strict_types=1);

namespace App\Framework\ErrorReporting;

use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\Next;
use App\Framework\Http\Request;
use App\Framework\Http\RequestStateManager;
use Throwable;

/**
 * Middleware for automatic error reporting with request context enrichment
 */
final readonly class ErrorReportingMiddleware implements HttpMiddleware
{
    private const int MAX_RECURSION_DEPTH = 5;
    private const int MAX_ARRAY_SIZE = 50;
    private const int MAX_STRING_LENGTH = 1000;

    private const array PROXY_HEADERS = [
        'X-Forwarded-For',
        'X-Real-IP',
        'Client-IP',
        'X-Forwarded',
        'Forwarded-For',
        'Forwarded',
    ];

    private const array SENSITIVE_KEYS = [
        'password', 'passwd', 'pwd', 'secret', 'token', 'key', 'api_key',
        'authorization', 'auth', 'csrf', 'csrf_token', 'access_token',
        'refresh_token', 'private_key', 'credit_card', 'card_number',
        'cvv', 'cvc', 'ssn', 'social_security',
    ];

    public function __construct(
        private ErrorReporter $reporter,
        private bool $enabled = true
    ) {
    }

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

        try {
            return $next($context);
        } catch (Throwable $e) {
            $this->reportError($e, $context);

            throw $e; // Re-throw for proper error handling chain
        }
    }

    /**
     * Report error with enriched request context
     */
    private function reportError(Throwable $error, MiddlewareContext $context): void
    {
        try {
            $request = $context->request;
            $requestContext = $this->buildRequestContext($request);

            $contextualReporter = $this->reporter->withRequestContext(
                method: $requestContext['method'],
                route: $requestContext['route'],
                requestId: $requestContext['requestId'],
                userAgent: $requestContext['userAgent'],
                ipAddress: $requestContext['ipAddress'],
                requestData: $requestContext['requestData']
            );

            $contextualReporter->reportThrowable($error);
        } catch (Throwable) {
            // Silently fail to prevent error reporting from breaking the application
        }
    }

    /**
     * Build comprehensive request context for error reporting
     */
    private function buildRequestContext(Request $request): array
    {
        return [
            'method' => $request->method->value,
            'route' => $request->path,
            'requestId' => $request->id->toString(),
            'userAgent' => $request->server->getUserAgent()->value,
            'ipAddress' => $this->extractClientIpAddress($request),
            'requestData' => $this->extractRequestData($request),
        ];
    }

    /**
     * Extract client IP address from request headers and server environment
     */
    private function extractClientIpAddress(Request $request): ?string
    {
        // Check proxy headers for real client IP
        foreach (self::PROXY_HEADERS as $header) {
            $ip = $this->extractIpFromHeader($request, $header);
            if ($ip !== null) {
                return $ip;
            }
        }

        // Fallback to direct connection IP
        return $request->server->get('REMOTE_ADDR');
    }

    /**
     * Extract and validate IP address from specific header
     */
    private function extractIpFromHeader(Request $request, string $header): ?string
    {
        $headerValue = $request->headers->get($header);
        if (empty($headerValue)) {
            return null;
        }

        // Handle comma-separated list (X-Forwarded-For)
        if (str_contains($headerValue, ',')) {
            $headerValue = trim(explode(',', $headerValue)[0]);
        }

        // Validate as public IP address
        return filter_var($headerValue, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)
            ? $headerValue
            : null;
    }

    /**
     * Extract and sanitize request data from multiple sources
     */
    private function extractRequestData(Request $request): ?array
    {
        $data = [];

        // Query parameters
        if (! empty($request->queryParams)) {
            $data['query'] = $this->sanitizeData($request->queryParams);
        }

        // Request body (could be form data or JSON)
        if (! empty($request->body)) {
            $data['body'] = $this->sanitizeRequestBody($request);
        }

        return ! empty($data) ? $data : null;
    }

    /**
     * Sanitize request body based on content type
     */
    private function sanitizeRequestBody(Request $request): mixed
    {
        $contentType = $request->headers->get('Content-Type') ?? '';

        if (str_contains($contentType, 'application/json')) {
            return $this->sanitizeJsonBody($request->body);
        }

        // For other content types, just sanitize as string
        return $this->sanitizeData($request->body);
    }

    /**
     * Parse and sanitize JSON body
     */
    private function sanitizeJsonBody(string $body): mixed
    {
        try {
            $jsonData = json_decode($body, true);

            return json_last_error() === JSON_ERROR_NONE
                ? $this->sanitizeData($jsonData)
                : '[invalid_json]';
        } catch (Throwable) {
            return '[json_parse_error]';
        }
    }

    /**
     * Recursively sanitize data to remove sensitive information
     */
    private function sanitizeData(mixed $data, int $depth = 0): mixed
    {
        if ($depth > self::MAX_RECURSION_DEPTH) {
            return '[max_depth_reached]';
        }

        return match (true) {
            is_array($data) => $this->sanitizeArray($data, $depth),
            is_string($data) => $this->sanitizeString($data),
            default => $data
        };
    }

    /**
     * Sanitize array data with size limits and sensitive key filtering
     */
    private function sanitizeArray(array $data, int $depth): array
    {
        // Limit array size to prevent memory issues
        if (count($data) > self::MAX_ARRAY_SIZE) {
            $data = array_slice($data, 0, self::MAX_ARRAY_SIZE, true);
            $data['[truncated]'] = 'Array truncated - too many items';
        }

        $sanitized = [];
        foreach ($data as $key => $value) {
            $sanitized[$key] = $this->isSensitiveKey((string)$key)
                ? '[redacted]'
                : $this->sanitizeData($value, $depth + 1);
        }

        return $sanitized;
    }

    /**
     * Sanitize string data with length limits and pattern removal
     */
    private function sanitizeString(string $data): string
    {
        // Limit string length
        if (strlen($data) > self::MAX_STRING_LENGTH) {
            $data = substr($data, 0, self::MAX_STRING_LENGTH) . '[truncated]';
        }

        // Remove sensitive patterns
        $patterns = [
            '/\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/' => '[card_number]',
            '/\b\d{3}-?\d{2}-?\d{4}\b/' => '[ssn]',
            '/Bearer\s+[A-Za-z0-9\-._~+\/]+=*/' => 'Bearer [redacted]',
        ];

        return preg_replace(array_keys($patterns), array_values($patterns), $data);
    }

    /**
     * Check if a key contains sensitive information that should be redacted
     */
    private function isSensitiveKey(string $key): bool
    {
        $lowerKey = strtolower($key);

        foreach (self::SENSITIVE_KEYS as $sensitiveKey) {
            if (str_contains($lowerKey, $sensitiveKey)) {
                return true;
            }
        }

        return false;
    }
}
