<?php

namespace AG\ElasticApmLaravel\Collectors;

use AG\ElasticApmLaravel\Agent;
use AG\ElasticApmLaravel\Contracts\DataCollector;
use AG\ElasticApmLaravel\EventClock;
use AG\ElasticApmLaravel\Helpers\StacktraceExtractor;
use Illuminate\Config\Repository as Config;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Nipwaayoni\Events\Transaction;
use Nipwaayoni\Exception\Transaction\UnknownTransactionException;

/**
 * Abstract class that provides base functionality to measure
 * events dispatched by the framework or your application code.
 */
abstract class EventDataCollector implements DataCollector
{
    /** @var Application */
    protected $app;

    /** @var Collection */
    protected $started_measures;

    /** @var Collection */
    protected $measures;

    /** @var Config */
    protected $config;

    /** @var RequestStartTime */
    protected $start_time;

    /** @var EventCounter */
    protected $event_counter;

    /** @var EventClock */
    protected $event_clock;

    /** @var Agent */
    protected $agent;

    final public function __construct(Application $app, Config $config, RequestStartTime $start_time, EventCounter $event_counter, EventClock $event_clock)
    {
        $this->app = $app;
        $this->config = $config;
        $this->start_time = $start_time;
        $this->event_counter = $event_counter;
        $this->event_clock = $event_clock;

        $this->started_measures = new Collection();
        $this->measures = new Collection();

        $this->registerEventListeners();
    }

    public function useAgent(Agent $agent): void
    {
        $this->agent = $agent;
    }

    /**
     * Starts a measure.
     */
    public function startMeasure(
        string $name,
        string $type = 'request',
        ?string $action = null,
        ?string $label = null,
        ?float $start_time = null,
    ): void {
        $start = $start_time ?? $this->event_clock->microtime();
        if ($this->hasStartedMeasure($name)) {
            Log::warning("Did not start measure '{$name}' because it's already started.");

            return;
        }

        $transactionStart = $this->start_time->microseconds();
        $data = [
            'label' => $label ?: $name,
            'start' => $start - $transactionStart,
            'type' => $type,
            'action' => $action,
            'exceeds_limit' => $this->event_counter->reachedLimit(),
        ];

        $this->started_measures->put($name, $data);

        $this->event_counter->increment();
    }

    /**
     * Check if a measure exists.
     */
    public function hasStartedMeasure(string $name): bool
    {
        return $this->started_measures->has($name);
    }

    /**
     * Stops a measure.
     */
    public function stopMeasure(string $name, array $params = []): void
    {
        $end = $this->event_clock->microtime();
        if (!$this->hasStartedMeasure($name)) {
            Log::warning("Did not stop measure '{$name}' because it hasn't been started.");

            return;
        }

        $measure = $this->started_measures->pull($name);

        if ($measure['exceeds_limit']) {
            return;
        }

        // Use the private pushMeasure() method since using addMeasure would reject valid measures
        // created before the limit was reached
        $this->pushMeasure(
            $measure['label'],
            $measure['start'],
            $end - $this->start_time->microseconds(),
            $measure['type'],
            $measure['action'],
            $params
        );
    }

    /**
     * Adds a measure.
     */
    public function addMeasure(
        string $label,
        float $start,
        float $end,
        string $type = 'request',
        ?string $action = 'request',
        ?array $context = [],
    ): void {
        if ($this->event_counter->reachedLimit()) {
            return;
        }

        $this->pushMeasure(
            $label,
            $start,
            $end,
            $type,
            $action,
            $context
        );

        $this->event_counter->increment();
    }

    // Only other exposed methods may push measures, this ensures the limit is respected
    private function pushMeasure(
        string $label,
        float $start,
        float $end,
        string $type = 'request',
        ?string $action = 'request',
        ?array $context = [],
    ): void {
        $this->measures->push([
            'label' => $label,
            'start' => $this->toMilliseconds($start),
            'duration' => $this->toMilliseconds($end - $start),
            'type' => $type,
            'action' => $action,
            'context' => $context,
            'stacktrace' => StacktraceExtractor::getStacktrace($this->config),
        ]);
    }

    public function collect(): Collection
    {
        $this->started_measures->keys()->each(function ($name) {
            $this->stopMeasure($name);
        });

        return $this->measures;
    }

    private function toMilliseconds(float $time): float
    {
        return round($time * 1000, 3);
    }

    public function reset(): void
    {
        $this->started_measures = new Collection();
        $this->measures = new Collection();
    }

    protected function shouldIgnoreTransaction(string $transaction_name): bool
    {
        $pattern = $this->config->get('elastic-apm-laravel.transactions.ignorePatterns');

        return $pattern && preg_match($pattern, $transaction_name);
    }

    protected function getTransaction(string $transaction_name): ?Transaction
    {
        try {
            return $this->agent->getTransaction($transaction_name);
        } catch (UnknownTransactionException $e) {
            return null;
        }
    }
}
