<?php

namespace Spatie\FlareClient;

use ErrorException;
use Spatie\Backtrace\Arguments\ArgumentReducers;
use Spatie\Backtrace\Arguments\Reducers\ArgumentReducer;
use Spatie\Backtrace\Backtrace;
use Spatie\Backtrace\Frame as SpatieFrame;
use Spatie\ErrorSolutions\Contracts\Solution;
use Spatie\FlareClient\Concerns\HasContext;
use Spatie\FlareClient\Concerns\UsesTime;
use Spatie\FlareClient\Context\ContextProvider;
use Spatie\FlareClient\Contracts\ProvidesFlareContext;
use Spatie\FlareClient\Glows\Glow;
use Spatie\FlareClient\Solutions\ReportSolution;
use Spatie\Ignition\Contracts\Solution as IgnitionSolution;
use Spatie\LaravelFlare\Exceptions\ViewException;
use Spatie\LaravelIgnition\Exceptions\ViewException as IgnitionViewException;
use Throwable;

class Report
{
    use UsesTime;
    use HasContext;

    protected Backtrace $stacktrace;

    protected string $exceptionClass = '';

    protected string $message = '';

    /** @var array<int, array{time: int, name: string, message_level: string, meta_data: array, microtime: float}> */
    protected array $glows = [];

    /** @var array<int, array<int|string, mixed>> */
    protected array $solutions = [];

    /** @var array<int, string> */
    public array $documentationLinks = [];

    protected ContextProvider $context;

    protected ?string $applicationPath = null;

    protected ?string $applicationVersion = null;

    /** @var array<int|string, mixed> */
    protected array $userProvidedContext = [];

    /** @var array<int|string, mixed> */
    protected array $exceptionContext = [];

    protected ?Throwable $throwable = null;

    protected string $notifierName = 'Flare Client';

    protected ?string $languageVersion = null;

    protected ?string $frameworkVersion = null;

    protected ?int $openFrameIndex = null;

    protected string $trackingUuid;

    protected ?View $view;

    public static ?string $fakeTrackingUuid = null;

    protected ?bool $handled = null;

    protected ?string $overriddenGrouping = null;

    /**
     * @param array<class-string<ArgumentReducer>|ArgumentReducer>|ArgumentReducers|null $argumentReducers
     * @param array<class-string, string> $overriddenGroupings
     */
    public static function createForThrowable(
        Throwable $throwable,
        ContextProvider $context,
        ?string $applicationPath = null,
        ?string $version = null,
        null|array|ArgumentReducers $argumentReducers = null,
        bool $withStackTraceArguments = true,
        array $overriddenGroupings = [],
    ): self {
        $stacktrace = Backtrace::createForThrowable($throwable)
            ->withArguments($withStackTraceArguments)
            ->reduceArguments($argumentReducers)
            ->applicationPath($applicationPath ?? '');

        $exceptionClass = self::getClassForThrowable($throwable);

        $report = (new self())
            ->setApplicationPath($applicationPath)
            ->throwable($throwable)
            ->useContext($context)
            ->exceptionClass($exceptionClass)
            ->message($throwable->getMessage())
            ->stackTrace($stacktrace)
            ->exceptionContext($throwable)
            ->setApplicationVersion($version);

        if (array_key_exists($exceptionClass, $overriddenGroupings)) {
            $report->overriddenGrouping($overriddenGroupings[$exceptionClass]);
        }

        return $report;
    }

    protected static function getClassForThrowable(Throwable $throwable): string
    {
        /** @phpstan-ignore-next-line */
        if ($throwable::class === IgnitionViewException::class || $throwable::class === ViewException::class) {
            /** @phpstan-ignore-next-line */
            if ($previous = $throwable->getPrevious()) {
                return get_class($previous);
            }
        }

        return get_class($throwable);
    }

    /** @param array<class-string<ArgumentReducer>|ArgumentReducer>|ArgumentReducers|null $argumentReducers */
    public static function createForMessage(
        string $message,
        string $logLevel,
        ContextProvider $context,
        ?string $applicationPath = null,
        null|array|ArgumentReducers $argumentReducers = null,
        bool $withStackTraceArguments = true,
    ): self {
        $stacktrace = Backtrace::create()
            ->withArguments($withStackTraceArguments)
            ->reduceArguments($argumentReducers)
            ->applicationPath($applicationPath ?? '');

        return (new self())
            ->setApplicationPath($applicationPath)
            ->message($message)
            ->useContext($context)
            ->exceptionClass($logLevel)
            ->stacktrace($stacktrace)
            ->openFrameIndex($stacktrace->firstApplicationFrameIndex());
    }

    public function __construct()
    {
        $this->trackingUuid = self::$fakeTrackingUuid ?? $this->generateUuid();
    }

    public function trackingUuid(): string
    {
        return $this->trackingUuid;
    }

    public function exceptionClass(string $exceptionClass): self
    {
        $this->exceptionClass = $exceptionClass;

        return $this;
    }

    public function getExceptionClass(): string
    {
        return $this->exceptionClass;
    }

    public function throwable(Throwable $throwable): self
    {
        $this->throwable = $throwable;

        return $this;
    }

    public function getThrowable(): ?Throwable
    {
        return $this->throwable;
    }

    public function message(string $message): self
    {
        $this->message = $message;

        return $this;
    }

    public function getMessage(): string
    {
        return $this->message;
    }

    public function stacktrace(Backtrace $stacktrace): self
    {
        $this->stacktrace = $stacktrace;

        return $this;
    }

    public function getStacktrace(): Backtrace
    {
        return $this->stacktrace;
    }

    public function notifierName(string $notifierName): self
    {
        $this->notifierName = $notifierName;

        return $this;
    }

    public function languageVersion(string $languageVersion): self
    {
        $this->languageVersion = $languageVersion;

        return $this;
    }

    public function frameworkVersion(string $frameworkVersion): self
    {
        $this->frameworkVersion = $frameworkVersion;

        return $this;
    }

    public function useContext(ContextProvider $request): self
    {
        $this->context = $request;

        return $this;
    }

    public function openFrameIndex(?int $index): self
    {
        $this->openFrameIndex = $index;

        return $this;
    }

    public function setApplicationPath(?string $applicationPath): self
    {
        $this->applicationPath = $applicationPath;

        return $this;
    }

    public function getApplicationPath(): ?string
    {
        return $this->applicationPath;
    }

    public function setApplicationVersion(?string $applicationVersion): self
    {
        $this->applicationVersion = $applicationVersion;

        return $this;
    }

    public function getApplicationVersion(): ?string
    {
        return $this->applicationVersion;
    }

    public function view(?View $view): self
    {
        $this->view = $view;

        return $this;
    }

    public function addGlow(Glow $glow): self
    {
        $this->glows[] = $glow->toArray();

        return $this;
    }

    public function addSolution(Solution|IgnitionSolution $solution): self
    {
        $this->solutions[] = ReportSolution::fromSolution($solution)->toArray();

        return $this;
    }

    /**
     * @param array<int, string> $documentationLinks
     *
     * @return $this
     */
    public function addDocumentationLinks(array $documentationLinks): self
    {
        $this->documentationLinks = $documentationLinks;

        return $this;
    }

    /**
     * @param array<int|string, mixed> $userProvidedContext
     *
     * @return $this
     */
    public function userProvidedContext(array $userProvidedContext): self
    {
        $this->userProvidedContext = $userProvidedContext;

        return $this;
    }

    /**
     * @return array<int|string, mixed>
     */
    public function allContext(): array
    {
        $context = $this->context->toArray();

        $context = array_merge_recursive_distinct($context, $this->exceptionContext);

        return array_merge_recursive_distinct($context, $this->userProvidedContext);
    }

    public function handled(?bool $handled = true): self
    {
        $this->handled = $handled;

        return $this;
    }

    public function overriddenGrouping(?string $overriddenGrouping): self
    {
        $this->overriddenGrouping = $overriddenGrouping;

        return $this;
    }

    protected function exceptionContext(Throwable $throwable): self
    {
        if ($throwable instanceof ProvidesFlareContext) {
            $this->exceptionContext = $throwable->context();
        }

        return $this;
    }

    /**
     * @return array<int|string, mixed>
     */
    protected function stracktraceAsArray(): array
    {
        return array_map(
            fn (SpatieFrame $frame) => Frame::fromSpatieFrame($frame)->toArray(),
            $this->cleanupStackTraceForError($this->stacktrace->frames()),
        );
    }

    /**
     * @param array<SpatieFrame> $frames
     *
     * @return array<SpatieFrame>
     */
    protected function cleanupStackTraceForError(array $frames): array
    {
        if ($this->throwable === null || get_class($this->throwable) !== ErrorException::class) {
            return $frames;
        }

        $firstErrorFrameIndex = null;

        $restructuredFrames = array_values(array_slice($frames, 1)); // remove the first frame where error was created

        foreach ($restructuredFrames as $index => $frame) {
            if ($frame->file === $this->throwable->getFile()) {
                $firstErrorFrameIndex = $index;

                break;
            }
        }

        if ($firstErrorFrameIndex === null) {
            return $frames;
        }

        $restructuredFrames[$firstErrorFrameIndex]->arguments = null; // Remove error arguments

        return array_values(array_slice($restructuredFrames, $firstErrorFrameIndex));
    }

    /**
     * @return array<string, mixed>
     */
    public function toArray(): array
    {
        return [
            'notifier' => $this->notifierName ?? 'Flare Client',
            'language' => 'PHP',
            'framework_version' => $this->frameworkVersion,
            'language_version' => $this->languageVersion ?? phpversion(),
            'exception_class' => $this->exceptionClass,
            'seen_at' => $this->getCurrentTime(),
            'message' => $this->message,
            'glows' => $this->glows,
            'solutions' => $this->solutions,
            'documentation_links' => $this->documentationLinks,
            'stacktrace' => $this->stracktraceAsArray(),
            'context' => $this->allContext(),
            'stage' => $this->stage,
            'message_level' => $this->messageLevel,
            'open_frame_index' => $this->openFrameIndex,
            'application_path' => $this->applicationPath,
            'application_version' => $this->applicationVersion,
            'tracking_uuid' => $this->trackingUuid,
            'handled' => $this->handled,
            'overridden_grouping' => $this->overriddenGrouping,
        ];
    }

    /*
     * Found on https://stackoverflow.com/questions/2040240/php-function-to-generate-v4-uuid/15875555#15875555
     */
    protected function generateUuid(): string
    {
        // Generate 16 bytes (128 bits) of random data or use the data passed into the function.
        $data = random_bytes(16);

        // Set version to 0100
        $data[6] = chr(ord($data[6]) & 0x0f | 0x40);
        // Set bits 6-7 to 10
        $data[8] = chr(ord($data[8]) & 0x3f | 0x80);

        // Output the 36 character UUID.
        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
    }
}
