<?php

declare(strict_types=1);

namespace Bakame\Stackwatch\Exporter;

use Bakame\Stackwatch\AggregatedMetrics;
use Bakame\Stackwatch\Environment;
use Bakame\Stackwatch\Metrics;
use Bakame\Stackwatch\Profiler;
use Bakame\Stackwatch\Report;
use Bakame\Stackwatch\Result;
use Bakame\Stackwatch\Snapshot;
use Bakame\Stackwatch\Span;
use Bakame\Stackwatch\Statistics;
use Bakame\Stackwatch\Timeline;
use OpenTelemetry\API\Metrics\CounterInterface;
use OpenTelemetry\API\Metrics\HistogramInterface;
use OpenTelemetry\API\Metrics\MeterInterface;
use OpenTelemetry\API\Metrics\MeterProviderInterface;
use OpenTelemetry\API\Trace\Span as OtlSpan;
use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\TracerInterface;
use OpenTelemetry\Context\Context;
use OpenTelemetry\SDK\Trace\TracerProviderInterface;

use function is_callable;

final class OtlExporter implements Exporter
{
    private readonly TracerInterface $tracer;
    private readonly MeterInterface $meter;
    private readonly HistogramInterface $execTime;
    private readonly HistogramInterface $memoryUsage;
    private readonly HistogramInterface $memoryRss;
    private readonly CounterInterface $cpuUser;
    private readonly CounterInterface $cpuSystem;

    public function __construct(
        TracerProviderInterface $tracerProvider,
        MeterProviderInterface $meterProvider,
    ) {
        $this->tracer = $tracerProvider->getTracer('profiler-exporter');

        $this->meter = $meterProvider->getMeter('profiler-exporter');

        $this->execTime = $this->meter->createHistogram('code.execution_time', 'ns');
        $this->memoryUsage = $this->meter->createHistogram('process.memory.usage', 'bytes');
        $this->memoryRss = $this->meter->createHistogram('process.memory.rss', 'bytes');
        $this->cpuUser = $this->meter->createCounter('process.cpu.time.user', 'ns');
        $this->cpuSystem = $this->meter->createCounter('process.cpu.time.system', 'ns');
    }

    public function exportTimeline(Timeline $timeline, ?callable $filter = null): void
    {
        $parent = $this->tracer->spanBuilder('timeline-run')
            ->setAttribute('profiler.identifier', $timeline->identifier())
            ->startSpan();
        $scope = $parent->activate();
        $input = null === $filter ? $timeline : $timeline->filter($filter);

        try {
            foreach ($input as $snapshot) {
                $this->exportSnapshot($snapshot);
            }
        } finally {
            $scope->detach();
            $parent->end();
        }
    }

    public function exportSnapshot(Snapshot $snapshot): void
    {
        $activeSpan = OtlSpan::fromContext(Context::getCurrent());
        if (!$activeSpan->isRecording()) {
            return;
        }

        $seconds = (int) $snapshot->timestamp->format('U');
        $micros = (int) $snapshot->timestamp->format('u');
        $nanos = $seconds * 1_000_000_000 + $micros * 1000;

        $activeSpan->addEvent('snapshot', [
            'label' => $snapshot->label,
            'code.execution_time_ns' => $snapshot->hrtime,
            'process.memory.usage' => $snapshot->memoryUsage,
            'process.memory.rss' => $snapshot->realMemoryUsage,
            'process.memory.peak_usage' => $snapshot->peakMemoryUsage,
            'process.memory.peak_rss' => $snapshot->realPeakMemoryUsage,
            'process.cpu.time.user' => $snapshot->cpuUserTime,
            'process.cpu.time.system' => $snapshot->cpuSystemTime,
        ], $nanos);

        $labels = ['label' => $snapshot->label];
        $this->execTime->record($snapshot->hrtime, $labels);
        $this->memoryUsage->record($snapshot->memoryUsage, $labels);
        $this->memoryRss->record($snapshot->realMemoryUsage, $labels);
        $this->cpuUser->add($snapshot->cpuUserTime, $labels);
        $this->cpuSystem->add($snapshot->cpuSystemTime, $labels);
    }

    public function exportProfiler(Profiler $profiler, callable|string|null $label = null): void
    {
        $parent = $this->tracer->spanBuilder('profiler-run')
            ->setAttribute('profiler.identifier', $profiler->identifier())
            ->startSpan();
        $scope = $parent->activate();

        $input = match (true) {
            null === $label => $profiler,
            is_callable($label) => $profiler->filter($label),
            default => $profiler->getAll($label),
        };

        try {
            foreach ($input as $span) {
                $this->exportSpan($span);
            }
        } finally {
            $scope->detach();
            $parent->end();
        }
    }

    public function exportSpan(Result|Span $span): void
    {
        if ($span instanceof Result) {
            $span = $span->span;
        }

        $start = $span->start;
        $end = $span->end;

        $seconds = (int) $start->timestamp->format('U');
        $micros = (int) $start->timestamp->format('u');
        $startNano = $seconds * 1_000_000_000 + $micros * 1000;

        $seconds = (int) $end->timestamp->format('U');
        $micros = (int) $end->timestamp->format('u');
        $endNano = $seconds * 1_000_000_000 + $micros * 1000;

        $otlSpan = $this->tracer
            ->spanBuilder($span->label)
            ->setSpanKind(SpanKind::KIND_INTERNAL)
            ->setStartTimestamp($startNano)
            ->startSpan();

        $scope = $otlSpan->activate();
        try {
            $this->exportSnapshot($start);
            $this->exportMetrics($span->metrics);
            $this->exportSnapshot($end);
        } finally {
            $scope->detach();
            $otlSpan->end($endNano);
        }
    }

    public function exportEnvironment(Environment $environment): void
    {
        $activeSpan = OtlSpan::fromContext(Context::getCurrent());
        if (!$activeSpan->isRecording()) {
            return;
        }

        $activeSpan->addEvent('environment', [
            'type' => 'php_environment',
            'os' => $environment->os,
            'os_family' => $environment->osFamily,
            'hostname' => $environment->hostname,
            'machine' => $environment->machine,
            'php_int_size' => $environment->phpIntSize,
            'php_architecture' => $environment->phpArchitecture,
            'php_version' => $environment->phpVersion,
            'sapi' => $environment->sapi,
            'memory_limit' => $environment->memoryLimit,
            'raw_memory_limit' => $environment->rawMemoryLimit,
            'cpu_cores' => $environment->cpuCores,
            'total_disk' => $environment->totalDisk,
            'free_disk' => $environment->freeDisk,
        ]);
    }

    public function exportMetrics(AggregatedMetrics|Metrics $metrics): void
    {
        $labels = ['type' => $metrics instanceof AggregatedMetrics ? $metrics->type->value : null];
        $this->cpuUser->add($metrics->cpuTime, $labels);
        $this->execTime->record($metrics->executionTime, $labels);
        $this->memoryUsage->record($metrics->memoryUsage, $labels);
        $this->memoryRss->record($metrics->realMemoryUsage, $labels);

        // Keep growth metrics as trace events for visualization
        $activeSpan = OtlSpan::fromContext(Context::getCurrent());
        if (!$activeSpan->isRecording()) {
            return;
        }

        $activeSpan->addEvent('metric', [
            'type' => $metrics instanceof AggregatedMetrics ? $metrics->type->value : null,
            'cpu.time' => $metrics->cpuTime,
            'execution.time' => $metrics->executionTime,
            'memory.usage' => $metrics->memoryUsage,
            'memory.usage.real' => $metrics->realMemoryUsage,
            'memory.peak' => $metrics->peakMemoryUsage,
            'memory.peak.real' => $metrics->realPeakMemoryUsage,
            'memory.usage.growth' => $metrics->memoryUsageGrowth,
            'memory.usage.real.growth' => $metrics->realMemoryUsageGrowth,
            'memory.peak.growth' => $metrics->peakMemoryUsageGrowth,
            'memory.peak.real.growth' => $metrics->realPeakMemoryUsageGrowth,
        ]);
    }

    public function exportStatistics(Statistics $statistics): void
    {
        $activeSpan = OtlSpan::fromContext(Context::getCurrent());
        if (!$activeSpan->isRecording()) {
            return;
        }

        $activeSpan->addEvent('statistics', $statistics->toArray());

        if (! $statistics->type->isGrowth()) {
            $labels = ['type' => $statistics->type->value];
            $this->execTime->record($statistics->average, $labels);
            $this->memoryUsage->record($statistics->sum, $labels);
        }
    }

    public function exportReport(Report $report): void
    {
        $parent = $this->tracer->spanBuilder('report-run')
            ->startSpan();
        $scope = $parent->activate();

        try {
            foreach ($report->jsonSerialize() as $stats) {
                $this->exportStatistics($stats);
            }
        } finally {
            $scope->detach();
            $parent->end();
        }
    }
}
