<?php

declare(strict_types=1);

namespace CpCreation\VitiCore\Monitoring\Error;

use Ramsey\Uuid\Uuid;
use Symfony\Component\HttpFoundation\Request;

final readonly class ErrorReportFactory
{
    private const int MAX_MESSAGE_LENGTH = 2_000;
    private const int MAX_TRACE_FRAMES = 16;

    public function __construct(
        private ErrorMonitoringConfiguration $configuration,
        private string $kernelProjectDir,
        private string $kernelEnvironment,
    ) {}

    /**
     * @return array{
     *     schema_version: string,
     *     event_id: string,
     *     instance_id: string,
     *     fingerprint: string,
     *     occurred_at: string,
     *     status_code: int,
     *     exception_class: string,
     *     message: string,
     *     file: ?string,
     *     line: ?int,
     *     route: ?string,
     *     method: string,
     *     environment: string,
     *     request_id: string,
     *     trace: list<array{file: ?string, line: ?int, call: string}>
     * }
     */
    public function create(\Throwable $throwable, Request $request, int $statusCode): array
    {
        $file = $this->normalizeFile($throwable->getFile());
        $route = $request->attributes->get('_route');
        $route = \is_string($route) && '' !== mb_trim($route)
            ? mb_substr(mb_trim($route), 0, 255)
            : null;
        $requestId = $request->headers->get('X-Request-Id');
        if (!\is_string($requestId) || 1 !== preg_match('/^[A-Za-z0-9._:-]{1,128}$/D', $requestId)) {
            $requestId = Uuid::uuid7()->toString();
        }

        return [
            'schema_version' => '1.0',
            'event_id' => Uuid::uuid7()->toString(),
            'instance_id' => $this->configuration->instanceId(),
            'fingerprint' => hash('sha256', implode('|', [
                $throwable::class,
                $file ?? '',
                (string) $throwable->getLine(),
                $route ?? '',
            ])),
            'occurred_at' => new \DateTimeImmutable('now', new \DateTimeZone('UTC'))->format(\DATE_ATOM),
            'status_code' => $statusCode,
            'exception_class' => mb_substr($throwable::class, 0, 255),
            'message' => $this->cleanText($throwable->getMessage(), self::MAX_MESSAGE_LENGTH),
            'file' => $file,
            'line' => $throwable->getLine() > 0 ? $throwable->getLine() : null,
            'route' => $route,
            'method' => mb_substr($request->getMethod(), 0, 16),
            'environment' => mb_substr($this->kernelEnvironment, 0, 32),
            'request_id' => $requestId,
            'trace' => $this->trace($throwable),
        ];
    }

    /** @return list<array{file: ?string, line: ?int, call: string}> */
    private function trace(\Throwable $throwable): array
    {
        $frames = [];
        foreach (array_slice($throwable->getTrace(), 0, self::MAX_TRACE_FRAMES) as $frame) {
            $class = \is_string($frame['class'] ?? null) ? $frame['class'] : '';
            $type = \is_string($frame['type'] ?? null) ? $frame['type'] : '';
            $function = \is_string($frame['function'] ?? null) ? $frame['function'] : '';
            $line = \is_int($frame['line'] ?? null) && $frame['line'] > 0 ? $frame['line'] : null;

            $frames[] = [
                'file' => \is_string($frame['file'] ?? null) ? $this->normalizeFile($frame['file']) : null,
                'line' => $line,
                'call' => mb_substr($class . $type . $function, 0, 512),
            ];
        }

        return $frames;
    }

    private function normalizeFile(string $file): ?string
    {
        $file = str_replace('\\', '/', mb_trim($file));
        if ('' === $file) {
            return null;
        }

        $projectDir = mb_rtrim(str_replace('\\', '/', $this->kernelProjectDir), '/');
        if (str_starts_with($file, $projectDir . '/')) {
            $file = mb_substr($file, mb_strlen($projectDir) + 1);
        } else {
            $file = basename($file);
        }

        return mb_substr($file, 0, 2_048);
    }

    private function cleanText(string $value, int $maximumLength): string
    {
        $value = str_replace("\0", '', mb_trim($value));

        return mb_substr($value, 0, $maximumLength);
    }
}
