<?php

declare(strict_types=1);

namespace Plugin\tnt_stabila_shop\src\Services;

use Plugin\tnt_stabila_shop\src\Repositories\SentrySpoolRepository;
use Sentry\Client;
use Sentry\HttpClient\HttpClient;
use Sentry\HttpClient\HttpClientInterface;
use Sentry\HttpClient\Request;
use Sentry\Options;

/**
 * Drains the Sentry spool out-of-band.
 *
 * Runs from the dedicated tnt_stabila_sentry_spool cron (never a customer request): it replays
 * each spooled envelope to the merchant's DSN through the SDK's real HTTP client. Delivered rows
 * are deleted; failures increment an attempt counter so a persistently unreachable endpoint is
 * eventually given up on (and purged) instead of retried forever. A blank DSN is a no-op.
 */
final class SentrySpoolFlusher
{
    /** Max envelopes shipped per cron run — bounds one run's work even after a long outage. */
    public const DEFAULT_BATCH_SIZE = 50;
    /** Give up on an envelope after this many failed sends. */
    public const DEFAULT_MAX_ATTEMPTS = 5;
    /** Envelopes older than this (whatever their attempts) are purged as stale. */
    public const STALE_AFTER = '-7 days';

    private const HTTP_TIMEOUT_SECONDS         = 5;
    private const HTTP_CONNECT_TIMEOUT_SECONDS = 3;

    private readonly HttpClientInterface $client;

    public function __construct(
        private readonly SentrySpoolRepository $spool,
        private readonly string $dsn,
        ?HttpClientInterface $client = null
    ) {
        $this->client = $client ?? new HttpClient(Client::SDK_IDENTIFIER, Client::SDK_VERSION);
    }

    /**
     * Ship up to $batchSize spooled envelopes.
     *
     * @return int number of envelopes successfully delivered
     */
    public function flush(
        int $batchSize = self::DEFAULT_BATCH_SIZE,
        int $maxAttempts = self::DEFAULT_MAX_ATTEMPTS,
        string $staleThreshold = ''
    ): int {
        $this->spool->purgeUndeliverable(
            $maxAttempts,
            $staleThreshold !== '' ? $staleThreshold : \date('Y-m-d H:i:s', \strtotime(self::STALE_AFTER))
        );
        if (\trim($this->dsn) === '') {
            return 0;
        }
        $options   = new Options([
            'dsn'                  => $this->dsn,
            'http_timeout'         => self::HTTP_TIMEOUT_SECONDS,
            'http_connect_timeout' => self::HTTP_CONNECT_TIMEOUT_SECONDS,
        ]);
        $delivered = 0;
        foreach ($this->spool->fetchBatch($batchSize, $maxAttempts) as $row) {
            $id = (int)$row->id;
            try {
                $request = new Request();
                $request->setStringBody((string)$row->body);
                if ($this->client->sendRequest($request, $options)->isSuccess()) {
                    $this->spool->delete($id);
                    $delivered++;

                    continue;
                }
            } catch (\Throwable) {
                // fall through to markFailed: a bad row must never abort the whole drain
            }
            $this->spool->markFailed($id);
        }

        return $delivered;
    }
}
