<?php

declare(strict_types=1);

namespace App\Framework\Tracing;

use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Random\RandomGenerator;

final class TraceContext
{
    private static ?TraceContext $current = null;

    private array $spans = [];

    private ?TraceSpan $activeSpan = null;

    public function __construct(
        private readonly string $traceId,
        private readonly float $startTime,
        private readonly RandomGenerator $randomGenerator
    ) {
    }

    public static function start(RandomGenerator $randomGenerator, ?string $traceId = null): self
    {
        $traceId ??= self::generateTraceId($randomGenerator);
        self::$current = new self($traceId, microtime(true), $randomGenerator);

        return self::$current;
    }

    public static function current(): ?self
    {
        return self::$current;
    }

    public static function clear(): void
    {
        self::$current = null;
    }

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

    public function getStartTime(): float
    {
        return $this->startTime;
    }

    public function startSpan(
        string $name,
        string $operation = 'unknown',
        array $tags = []
    ): TraceSpan {
        $span = new TraceSpan(
            spanId: self::generateSpanId($this->randomGenerator),
            traceId: $this->traceId,
            parentSpanId: $this->activeSpan?->spanId,
            name: $name,
            operation: $operation,
            startTime: microtime(true),
            tags: $tags
        );

        $this->spans[] = $span;
        $this->activeSpan = $span;

        return $span;
    }

    public function finishSpan(TraceSpan $span): void
    {
        $span->finish();

        // If this was the active span, set parent as active
        if ($this->activeSpan === $span) {
            $this->activeSpan = $this->findParentSpan($span);
        }
    }

    public function getActiveSpan(): ?TraceSpan
    {
        return $this->activeSpan;
    }

    public function getAllSpans(): array
    {
        return $this->spans;
    }

    public function getFinishedSpans(): array
    {
        return array_filter($this->spans, fn (TraceSpan $span) => $span->isFinished());
    }

    public function getTotalDuration(): Duration
    {
        $endTime = microtime(true);

        return Duration::fromSeconds($endTime - $this->startTime);
    }

    public function addEvent(string $name, array $attributes = []): void
    {
        if ($this->activeSpan) {
            $this->activeSpan->addEvent($name, $attributes);
        }
    }

    public function addTag(string $key, mixed $value): void
    {
        if ($this->activeSpan) {
            $this->activeSpan->addTag($key, $value);
        }
    }

    public function recordError(\Throwable $error): void
    {
        if ($this->activeSpan) {
            $this->activeSpan->recordError($error);
        }
    }

    public function toArray(): array
    {
        return [
            'traceId' => $this->traceId,
            'startTime' => $this->startTime,
            'duration' => $this->getTotalDuration()->toSeconds(),
            'spans' => array_map(fn (TraceSpan $span) => $span->toArray(), $this->spans),
        ];
    }

    private function findParentSpan(TraceSpan $span): ?TraceSpan
    {
        $parentId = $span->parentSpanId;
        if ($parentId === null) {
            return null;
        }

        foreach ($this->spans as $s) {
            if ($s->spanId === $parentId && ! $s->isFinished()) {
                return $s;
            }
        }

        return null;
    }

    private static function generateTraceId(RandomGenerator $randomGenerator): string
    {
        return bin2hex($randomGenerator->bytes(16));
    }

    private static function generateSpanId(RandomGenerator $randomGenerator): string
    {
        return bin2hex($randomGenerator->bytes(8));
    }
}
