<?php declare(strict_types=1);

namespace Rollbar;

use Monolog\Handler\ErrorLogHandler;
use Monolog\Handler\NoopHandler;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use Rollbar\Payload\Level;
use Rollbar\Payload\Payload;
use Rollbar\Payload\EncodedPayload;
use Rollbar\Senders\AgentSender;
use Rollbar\Senders\CurlSender;
use Rollbar\Senders\SenderInterface;
use Throwable;
use Rollbar\Senders\FluentSender;

class Config
{
    use UtilitiesTrait;

    const VERBOSE_NONE     = 'none';
    const VERBOSE_NONE_INT = 1000;

    private static array $options = array(
        'access_token',
        'agent_log_location',
        'allow_exec',
        'endpoint',
        'base_api_url',
        'autodetect_branch',
        'branch',
        'capture_error_stacktraces',
        'check_ignore',
        'code_version',
        'custom',
        'custom_data_method',
        'enabled',
        'environment',
        'error_sample_rates',
        'exception_sample_rates',
        'fluent_host',
        'fluent_port',
        'fluent_tag',
        'handler',
        'host',
        'include_error_code_context',
        'include_exception_code_context',
        'included_errno',
        'log_payload',
        'log_payload_logger',
        'person',
        'person_fn',
        'capture_ip',
        'capture_username',
        'capture_email',
        'root',
        'scrubber',
        'scrub_fields',
        'scrub_safelist',
        'timeout',
        'transmit',
        'custom_truncation',
        'report_suppressed',
        'use_error_reporting',
        'proxy',
        'send_message_trace',
        'include_raw_request_body',
        'local_vars_dump',
        'max_nesting_depth',
        'max_items',
        'minimum_level',
        'verbose',
        'verbose_logger',
        'raise_on_error',
        'transformer',
    );

    private string $accessToken;

    /**
     * @var boolean $enabled If this is false then do absolutely nothing,
     * try to be as close to the scenario where Rollbar did not exist at
     * all in the code.
     * Default: true
     */
    private bool $enabled = true;

    /**
     * @var boolean $transmit If this is false then we do everything except
     * make the post request at the end of the pipeline.
     * Default: true
     */
    private bool $transmit;

    /**
     * @var boolean $logPayload If this is true then we output the payload to
     * standard out or a configured logger right before transmitting.
     * Default: false
     */
    private bool $logPayload;

    /**
     * @var LoggerInterface $logPayloadLogger Logger responsible for logging request
     * payload and response dumps on. The messages logged can be controlled with
     * `log_payload` config options.
     * Default: \Monolog\Logger with \Monolog\Handler\ErrorLogHandler
     */
    private LoggerInterface $logPayloadLogger;

    /**
     * @var string $verbose If this is set to any of the \Psr\Log\LogLevel options
     * then we output messages related to the processing of items that might be
     * useful to someone trying to understand what Rollbar is doing. The logged
     * messages are dependent on the level of verbosity. The supported options are
     * all the log levels of \Psr\Log\LogLevel
     * (https://github.com/php-fig/log/blob/master/Psr/Log/LogLevel.php) plus
     * an additional Rollbar\Config::VERBOSE_NONE option which makes the SDK quiet
     * (excluding `log_payload` option configured separetely).
     * Essentially this option controls the level of verbosity of the default
     * `verbose_logger`. If you override the default `verbose_logger`, you need
     * to implement obeying the `verbose` config option yourself.
     * Default: Rollbar\Config::VERBOSE_NONE
     */
    private string $verbose;

    /**
     * @var LoggerInterface $versbosity_logger The logger object used to log
     * the internal messages of the SDK. The verbosity level of the default
     * $verbosityLogger can be controlled with `verbose` config option.
     * Default: \Rollbar\VerboseLogger
     */
    private LoggerInterface $verboseLogger;

    /**
     * @var DataBuilder
     */
    private $dataBuilder;
    private $configArray;

    /**
     * @var TransformerInterface
     */
    private ?TransformerInterface $transformer = null;
    /**
     * @var FilterInterface
     */
    private $filter;

    /**
     * @var int
     */
    private int $minimumLevel;

    /**
     * @var ResponseHandlerInterface
     */
    private $responseHandler;
    /**
     * @var \Rollbar\Senders\SenderInterface
     */
    private ?SenderInterface $sender = null;
    private $reportSuppressed;
    /**
     * @var Scrubber
     */
    private $scrubber;

    private bool $batched = false;
    private int $batchSize = 50;

    private int $maxNestingDepth = 10;

    private array $custom = array();

    /**
     * @var callable with parameters $toLog, $contextDataMethodContext. The return
     * value of the callable will be appended to the custom field of the item.
     */
    private $customDataMethod;

    /**
     * @var callable
     */
    private $checkIgnore;
    private $errorSampleRates;
    private $exceptionSampleRates;
    private $mtRandmax;

    private $includedErrno;

    /**
     * @var bool Sets whether to respect current {@see error_reporting()} level or not.
     */
    private bool $useErrorReporting = false;

    /**
     * @var boolean Should debug_backtrace() data be sent with string messages
     * sent through RollbarLogger::log().
     */
    private bool $sendMessageTrace = false;

    /**
     * The fully qualified class name of a custom truncation strategy class or null if no custom class is specified.
     * The class should implement {@see \Rollbar\Truncation\StrategyInterface}.
     *
     * @var string|null $customTruncation
     *
     * @since 1.6.0
     * @since 4.0.0 Added string|null type, and defaults to null.
     */
    private ?string $customTruncation = null;

    /**
     * @var boolean Should the SDK raise an exception after logging an error.
     * This is useful in test and development enviroments.
     * https://github.com/rollbar/rollbar-php/issues/448
     */
    private bool $raiseOnError = false;

    /**
     * @var int The maximum number of items reported to Rollbar within one
     * request.
     */
    private int $maxItems;

    public function __construct(array $configArray)
    {
        $this->includedErrno = Defaults::get()->includedErrno();

        $this->updateConfig($configArray);

        $this->errorSampleRates = Defaults::get()->errorSampleRates();
        if (isset($configArray['error_sample_rates'])) {
            $this->errorSampleRates = $configArray['error_sample_rates'];
        }

        $this->exceptionSampleRates = Defaults::get()->exceptionSampleRates();
        if (isset($configArray['exception_sample_rates'])) {
            $this->exceptionSampleRates = $configArray['exception_sample_rates'];
        }

        $levels = array(E_WARNING, E_NOTICE, E_USER_ERROR, E_USER_WARNING,
            E_USER_NOTICE, E_STRICT, E_RECOVERABLE_ERROR);
        // PHP 5.3.0
        if (defined('E_DEPRECATED')) {
            $levels = array_merge($levels, array(E_DEPRECATED, E_USER_DEPRECATED));
        }
        $curr = 1;
        for ($i = 0, $num = count($levels); $i < $num; $i++) {
            $level = $levels[$i];
            if (!isset($this->errorSampleRates[$level])) {
                $this->errorSampleRates[$level] = $curr;
            }
        }
        $this->mtRandmax = mt_getrandmax();
    }

    public static function listOptions(): array
    {
        return self::$options;
    }

    public function configure(array $config): void
    {
        $this->updateConfig($this->extend($config));
    }

    public function extend(array $config): array
    {
        return array_replace_recursive(array(), $this->configArray, $config);
    }

    public function getConfigArray(): array
    {
        return $this->configArray;
    }

    protected function updateConfig(array $config): void
    {
        $this->configArray = $config;

        $this->setEnabled($config);
        $this->setTransmit($config);
        $this->setLogPayload($config);
        $this->setLogPayloadLogger($config);
        $this->setVerbose($config);
        $this->setVerboseLogger($config);
        // The sender must be set before the access token, so we know if it is required.
        $this->setSender($config);
        $this->setAccessToken($config);
        $this->setDataBuilder($config);
        $this->setTransformer($config);
        $this->setMinimumLevel($config);
        $this->setReportSuppressed($config);
        $this->setFilters($config);
        $this->setScrubber($config);
        $this->setBatched($config);
        $this->setBatchSize($config);
        $this->setMaxNestingDepth($config);
        $this->setCustom($config);
        $this->setResponseHandler($config);
        $this->setCheckIgnoreFunction($config);
        $this->setSendMessageTrace($config);
        $this->setRaiseOnError($config);

        if (isset($config['included_errno'])) {
            $this->includedErrno = $config['included_errno'];
        }

        $this->useErrorReporting = Defaults::get()->useErrorReporting();
        if (isset($config['use_error_reporting'])) {
            $this->useErrorReporting = $config['use_error_reporting'];
        }

        $this->maxItems = Defaults::get()->maxItems();
        if (isset($config['max_items'])) {
            $this->maxItems = $config['max_items'];
        }

        if (isset($config['custom_truncation'])) {
            $this->customTruncation = $config['custom_truncation'];
        }

        $this->customDataMethod = Defaults::get()->customDataMethod();
        if (isset($config['custom_data_method'])) {
            $this->customDataMethod = $config['custom_data_method'];
        }
    }

    private function setAccessToken(array $config): void
    {
        if (isset($_ENV['ROLLBAR_ACCESS_TOKEN']) && !isset($config['access_token'])) {
            $config['access_token'] = $_ENV['ROLLBAR_ACCESS_TOKEN'];
        }
        
        $this->utilities()->validateString(
            $config['access_token'],
            "config['access_token']",
            32,
            !$this->sender->requireAccessToken(),
        );
        $this->accessToken = $config['access_token'] ?? '';
    }

    private function setEnabled(array $config): void
    {
        if (array_key_exists('enabled', $config) && $config['enabled'] === false) {
            $this->disable();
        } else {
            if (Defaults::get()->enabled() === false) {
                $this->disable();
            } else {
                $this->enable();
            }
        }
    }

    private function setTransmit(array $config): void
    {
        $this->transmit = $config['transmit'] ?? Defaults::get()->transmit();
    }

    private function setLogPayload(array $config): void
    {
        $this->logPayload = $config['log_payload'] ?? Defaults::get()->logPayload();
    }

    private function setLogPayloadLogger(array $config): void
    {
        $this->logPayloadLogger = $config['log_payload_logger'] ??
            new Logger('rollbar.payload', array(new ErrorLogHandler()));

        if (!($this->logPayloadLogger instanceof LoggerInterface)) {
            throw new \Exception('Log Payload Logger must implement \Psr\Log\LoggerInterface');
        }
    }

    private function setVerbose(array $config): void
    {
        $this->verbose = $config['verbose'] ?? Defaults::get()->verbose();
    }

    private function setVerboseLogger(array $config): void
    {
        if (isset($config['verbose_logger'])) {
            $this->verboseLogger = $config['verbose_logger'];
        } else {
            $verboseLevel = $this->verboseInteger();
            // The verboseLogger must be an instance of LoggerInterface. Setting
            // it to null would require every log call to check if it is null,
            // so we set it to a NoopHandler instead. The NoopHandler does what
            // you would expect and does nothing.
            //
            // Additionally, since Monolog v3 all log levels are defined in an
            // enum. This means that using a custom log level will throw an
            // exception. To avoid this we only set the level if it is not our
            // "custom" verbose level.
            //
            // Using a built-in level would cause the verbose logger to log
            // messages that are currently silent if the verbose log leve is set
            // to "none".
            if ($verboseLevel === self::VERBOSE_NONE_INT) {
                $handler = new NoopHandler();
            } else {
                $handler = new ErrorLogHandler();
                $handler->setLevel($verboseLevel);
            }
            $this->verboseLogger = new Logger('rollbar.verbose', array($handler));
        }

        if (!($this->verboseLogger instanceof LoggerInterface)) {
            throw new \Exception('Verbose logger must implement \Psr\Log\LoggerInterface');
        }
    }

    public function enable(): void
    {
        $this->enabled = true;
    }

    public function disable(): void
    {
        $this->enabled = false;
    }

    private function setDataBuilder(array $config): void
    {
        if (!isset($config['utilities'])) {
            $config['utilities'] = $this->utilities();
        }

        $exp = DataBuilderInterface::class;
        $def = DataBuilder::class;
        $this->setupWithOptions($config, "dataBuilder", $exp, $def, true);
    }

    private function setTransformer(array $config): void
    {
        $expected = TransformerInterface::class;
        $this->setupWithOptions($config, "transformer", $expected);
    }

    private function setMinimumLevel(array $config): void
    {
        $this->minimumLevel = \Rollbar\Defaults::get()->minimumLevel();

        $override = $config['minimum_level'] ?? null;
        $override = array_key_exists('minimumLevel', $config) ? $config['minimumLevel'] : $override;

        if ($override instanceof Level) {
            $this->minimumLevel = $override->toInt();
        } elseif (is_string($override)) {
            $level = LevelFactory::fromName($override);
            if ($level !== null) {
                $this->minimumLevel = $level->toInt();
            }
        } elseif (is_int($override)) {
            $this->minimumLevel = $override;
        }
    }

    private function setReportSuppressed(array $config): void
    {
        $this->reportSuppressed = isset($config['reportSuppressed']) && $config['reportSuppressed'];
        if (!$this->reportSuppressed) {
            $this->reportSuppressed = isset($config['report_suppressed']) && $config['report_suppressed'];
        }

        if (!$this->reportSuppressed) {
            $this->reportSuppressed = \Rollbar\Defaults::get()->reportSuppressed();
        }
    }

    private function setFilters(array $config): void
    {
        $this->setupWithOptions($config, "filter", FilterInterface::class);
    }

    private function setSender(array $config): void
    {
        $expected = SenderInterface::class;
        $default = CurlSender::class;

        $this->setTransportOptions($config);
        $default = $this->setAgentSenderOptions($config, $default);
        $default = $this->setFluentSenderOptions($config, $default);

        $this->setupWithOptions($config, "sender", $expected, $default);
    }

    private function setScrubber(array $config): void
    {
        $exp = ScrubberInterface::class;
        $def = Scrubber::class;
        $this->setupWithOptions($config, "scrubber", $exp, $def, true);
    }

    private function setBatched(array $config): void
    {
        if (array_key_exists('batched', $config)) {
            $this->batched = $config['batched'];
        }
    }

    private function setRaiseOnError(array $config): void
    {
        if (array_key_exists('raise_on_error', $config)) {
            $this->raiseOnError = $config['raise_on_error'];
        } else {
            $this->raiseOnError = \Rollbar\Defaults::get()->raiseOnError();
        }
    }

    private function setBatchSize(array $config): void
    {
        if (array_key_exists('batch_size', $config)) {
            $this->batchSize = $config['batch_size'];
        }
    }

    private function setMaxNestingDepth(array $config): void
    {
        if (array_key_exists('max_nesting_depth', $config)) {
            $this->maxNestingDepth = $config['max_nesting_depth'];
        }
    }

    public function setCustom(array $config): void
    {
        $this->dataBuilder->setCustom($config);
    }

    public function addCustom($key, $data)
    {
        $this->dataBuilder->addCustom($key, $data);
    }

    public function removeCustom($key)
    {
        $this->dataBuilder->removeCustom($key);
    }

    public function transmitting(): bool
    {
        return $this->transmit;
    }

    public function loggingPayload()
    {
        return $this->logPayload;
    }

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

    public function verboseInteger(): int
    {
        if ($this->verbose == self::VERBOSE_NONE) {
            return self::VERBOSE_NONE_INT;
        }
        /**
         * @psalm-suppress UndefinedClass
         * @psalm-suppress UndefinedDocblockClass
         * @var int|\Monolog\Level $level Monolog v2 returns an integer, v3 returns a \Monolog\Level enum.
         */
        $level = Logger::toMonologLevel($this->verbose);
        /**
         * @psalm-suppress UndefinedClass
         */
        if (is_a($level, '\Monolog\Level')) {
            return $level->value;
        }
        return $level;
    }

    public function getCustom()
    {
        return $this->dataBuilder->getCustom();
    }

    /**
     * Sets the custom truncation strategy class for payloads.
     *
     * @param string|null $type The fully qualified class name of the custom payload truncation strategy. The class
     *                          must implement {@see \Rollbar\Truncation\StrategyInterface}. If null is given any custom
     *                          strategy will be removed.
     *
     * @return void
     */
    public function setCustomTruncation(?string $type): void
    {
        $this->customTruncation = $type;
    }

    /**
     * Returns the fully qualified class name of the custom payload truncation strategy.
     *
     * @return string|null Will return null if a custom truncation strategy was not defined.
     *
     * @since 1.6.0
     * @since 4.0.0 Added may return null.
     */
    public function getCustomTruncation(): ?string
    {
        return $this->customTruncation;
    }

    private function setTransportOptions(array &$config): void
    {
        if (array_key_exists('base_api_url', $config)) {
            $config['senderOptions']['endpoint'] = $config['base_api_url'] . 'item/';
        }

        if (array_key_exists('endpoint', $config)) {
            $config['senderOptions']['endpoint'] = $config['endpoint'] . 'item/';
        }

        if (array_key_exists('timeout', $config)) {
            $config['senderOptions']['timeout'] = $config['timeout'];
        }

        if (array_key_exists('proxy', $config)) {
            $config['senderOptions']['proxy'] = $config['proxy'];
        }

        if (array_key_exists('ca_cert_path', $config)) {
            $config['senderOptions']['ca_cert_path'] = $config['ca_cert_path'];
        }
    }

    private function setAgentSenderOptions(array &$config, mixed $default): mixed
    {
        if (!array_key_exists('handler', $config) || $config['handler'] != 'agent') {
            return $default;
        }
        $default = AgentSender::class;
        if (array_key_exists('agent_log_location', $config)) {
            $config['senderOptions'] = array(
                'agentLogLocation' => $config['agent_log_location']
            );
        }
        return $default;
    }

    private function setFluentSenderOptions(array &$config, mixed $default): mixed
    {
        if (!isset($config['handler']) || $config['handler'] != 'fluent') {
            return $default;
        }
        $default = FluentSender::class;

        if (isset($config['fluent_host'])) {
            $config['senderOptions']['fluentHost'] = $config['fluent_host'];
        }

        if (isset($config['fluent_port'])) {
            $config['senderOptions']['fluentPort'] = $config['fluent_port'];
        }

        if (isset($config['fluent_tag'])) {
            $config['senderOptions']['fluentTag'] = $config['fluent_tag'];
        }

        return $default;
    }

    private function setResponseHandler(array $config): void
    {
        $this->setupWithOptions($config, "responseHandler", ResponseHandlerInterface::class);
    }

    private function setCheckIgnoreFunction(array $config): void
    {
        // Remain backwards compatible
        if (isset($config['checkIgnore'])) {
            $this->checkIgnore = $config['checkIgnore'];
        }

        if (isset($config['check_ignore'])) {
            $this->checkIgnore = $config['check_ignore'];
        }
    }

    private function setSendMessageTrace(array $config): void
    {
        if (!isset($config['send_message_trace'])) {
            return;
        }

        $this->sendMessageTrace = $config['send_message_trace'];
    }

    /**
     * Allows setting up configuration options that might be specified by class
     * name. Any interface used with `setupWithOptions` should be constructed
     * with a single parameter: an associative array with the config options.
     * It is assumed that it will be in the configuration as a sibling to the
     * key the class is named in. The options should have the same key as the
     * classname, but with 'Options' appended. E.g:
     * ```array(
     *   "sender" => "MySender",
     *   "senderOptions" => array(
     *     "speed" => 11,
     *     "protocol" => "First Contact"
     *   )
     * );```
     * Will be initialized as if you'd used:
     * `new MySender(array("speed"=>11,"protocol"=>"First Contact"));`
     * You can also just pass an instance in directly. (In which case options
     * are ignored)
     * @param array $config
     * @param string $keyName
     * @param string $expectedType
     * @param ?string $defaultClass
     * @param bool $passWholeConfig
     */
    protected function setupWithOptions(
        array $config,
        string $keyName,
        string $expectedType,
        ?string $defaultClass = null,
        bool $passWholeConfig = false
    ): void {

        $class = $config[$keyName] ?? null;

        if (is_null($defaultClass) && is_null($class)) {
            return;
        }

        if (is_null($class)) {
            $class = $defaultClass;
        }
        if (is_string($class)) {
            if ($passWholeConfig) {
                $options = $config;
            } else {
                $options = $config[$keyName . "Options"] ?? array();
            }
            $this->$keyName = new $class($options);
        } else {
            $this->$keyName = $class;
        }

        if (!$this->$keyName instanceof $expectedType) {
            throw new \InvalidArgumentException(
                "$keyName must be a $expectedType"
            );
        }
    }

    /**
     * Returns the logger responsible for logging request payload and response dumps, if enabled.
     *
     * @return LoggerInterface
     */
    public function logPayloadLogger(): LoggerInterface
    {
        return $this->logPayloadLogger;
    }

    public function verboseLogger(): LoggerInterface
    {
        return $this->verboseLogger;
    }

    public function getRollbarData($level, $toLog, $context)
    {
        return $this->dataBuilder->makeData($level, $toLog, $context);
    }

    public function getDataBuilder()
    {
        return $this->dataBuilder;
    }

    public function getSender()
    {
        return $this->sender;
    }

    public function getScrubber()
    {
        return $this->scrubber;
    }

    public function getBatched(): bool
    {
        return $this->batched;
    }

    public function getBatchSize(): int
    {
        return $this->batchSize;
    }

    public function getMaxNestingDepth(): int
    {
        return $this->maxNestingDepth;
    }

    public function getMaxItems(): int
    {
        return $this->maxItems;
    }

    public function getMinimumLevel(): int
    {
        return $this->minimumLevel;
    }

    public function getRaiseOnError(): bool
    {
        return $this->raiseOnError;
    }

    public function transform(
        Payload $payload,
        Level|string $level,
        mixed $toLog,
        array $context = array ()
    ): Payload {
        if (count($this->custom) > 0) {
            $this->verboseLogger()->debug("Adding custom data to the payload.");
            $data = $payload->getData();
            $custom = $data->getCustom();
            $custom = array_merge(array(), $this->custom, (array)$custom);
            $data->setCustom($custom);
            $payload->setData($data);
        }
        if (is_null($this->transformer)) {
            return $payload;
        }

        $this->verboseLogger()->debug("Applying transformer " . get_class($this->transformer) . " to the payload.");

        return $this->transformer->transform($payload, $level, $toLog, $context);
    }

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

    public function enabled(): bool
    {
        return $this->enabled === true;
    }

    public function disabled(): bool
    {
        return !$this->enabled();
    }

    public function getSendMessageTrace()
    {
        return $this->sendMessageTrace;
    }


    public function checkIgnored(Payload $payload, $toLog, bool $isUncaught)
    {
        if (isset($this->checkIgnore)) {
            try {
                $ok = ($this->checkIgnore)($isUncaught, $toLog, $payload);
                if ($ok) {
                    $this->verboseLogger()->info('Occurrence ignored due to custom check_ignore logic');
                    return true;
                }
            } catch (Throwable $exception) {
                $this->verboseLogger()->error(
                    'Exception occurred in the custom checkIgnore logic:' . $exception->getMessage()
                );
                $this->checkIgnore = null;
            }
        }

        if ($this->payloadLevelTooLow($payload)) {
            $this->verboseLogger()->debug("Occurrence's level is too low");
            return true;
        }

        if (!is_null($this->filter)) {
            $filter = $this->filter->shouldSend($payload, $isUncaught);
            $this->verboseLogger()->debug("Custom filter result: " . var_export($filter, true));
            return $filter;
        }

        return false;
    }

    public function internalCheckIgnored(string $level, mixed $toLog): bool
    {
        if ($this->shouldSuppress()) {
            $this->verboseLogger()->debug('Ignoring (error reporting has been disabled in PHP config)');
            return true;
        }

        if ($this->levelTooLow(LevelFactory::fromName($level))) {
            $this->verboseLogger()->debug("Occurrence's level is too low");
            return true;
        }

        if ($toLog instanceof ErrorWrapper) {
            return $this->shouldIgnoreErrorWrapper($toLog);
        }

        if ($toLog instanceof \Exception) {
            return $this->shouldIgnoreException($toLog);
        }

        return false;
    }

    /**
     * Check if the error should be ignored due to `includedErrno` config, `useErrorReporting` config or
     * `errorSampleRates` config.
     *
     * @param int $errno The PHP error level bitmask.
     *
     * @return bool
     */
    public function shouldIgnoreError(int $errno): bool
    {
        if ($this->useErrorReporting && ($errno & error_reporting()) === 0) {
            // ignore due to error_reporting level
            $this->verboseLogger()->debug("Ignore (error below allowed error_reporting level)");
            return true;
        }

        if ($this->includedErrno != -1 && ($errno & $this->includedErrno) != $errno) {
            // ignore
            $this->verboseLogger()->debug("Ignore due to included_errno level");
            return true;
        }

        if (isset($this->errorSampleRates[$errno])) {
            // get a float in the range [0, 1)
            // mt_rand() is inclusive, so add 1 to mt_randmax
            $float_rand = mt_rand() / ($this->mtRandmax + 1);
            if ($float_rand > $this->errorSampleRates[$errno]) {
                // skip
                $this->verboseLogger()->debug("Skip due to error sample rating");
                return true;
            }
        }

        return false;
    }

    /**
     * Check if the error should be ignored due to `included_errno` config,
     * `use_error_reporting` config or `error_sample_rates` config.
     *
     * @param \Rollbar\ErrorWrapper $toLog
     *
     * @return bool
     */
    protected function shouldIgnoreErrorWrapper(ErrorWrapper $toLog): bool
    {
        return $this->shouldIgnoreError($toLog->errorLevel);
    }

    /**
     * Check if the exception should be ignored due to configured exception
     * sample rates.
     *
     * @param \Exception $toLog
     *
     * @return bool
     */
    public function shouldIgnoreException(\Exception $toLog): bool
    {
        // get a float in the range [0, 1)
        // mt_rand() is inclusive, so add 1 to mt_randmax
        $floatRand = mt_rand() / ($this->mtRandmax + 1);
        if ($floatRand > $this->exceptionSampleRate($toLog)) {
            // skip
            $this->verboseLogger()->debug("Skip exception due to exception sample rating");
            return true;
        }

        return false;
    }

    /**
     * Calculate what's the chance of logging this exception according to
     * exception sampling.
     *
     * @param \Exception $toLog
     *
     * @return float
     */
    public function exceptionSampleRate(\Exception $toLog): float
    {
        $sampleRate = 1.0;
        if (count($this->exceptionSampleRates) == 0) {
            return $sampleRate;
        }

        $exceptionClasses = array();

        $class = get_class($toLog);
        while ($class) {
            $exceptionClasses []= $class;
            $class = get_parent_class($class);
        }
        $exceptionClasses = array_reverse($exceptionClasses);

        foreach ($exceptionClasses as $exceptionClass) {
            if (isset($this->exceptionSampleRates["$exceptionClass"])) {
                $sampleRate = $this->exceptionSampleRates["$exceptionClass"];
            }
        }

        return $sampleRate;
    }

    /**
     * @param Payload $payload
     * @return bool
     */
    private function payloadLevelTooLow(Payload $payload): bool
    {
        return $this->levelTooLow($payload->getData()->getLevel());
    }

    /**
     * @param Level $level
     * @return bool
     */
    private function levelTooLow(Level $level): bool
    {
        return $level->toInt() < $this->minimumLevel;
    }

    /**
     * Decides if a given log message should be suppressed by policy.
     * If so, then a debug message is emitted: "Ignoring (error reporting has been disabled in PHP config"
     * @since 3.0.1
     */
    public function shouldSuppress(): bool
    {
        // report_suppressed option forces reporting regardless of PHP settings.
        if ($this->reportSuppressed) {
            return false;
        }

        $errorReporting = error_reporting();

        // For error control operator of PHP 8:
        // > Prior to PHP 8.0.0, the error_reporting() called inside the
        // > custom error handler always returned 0 if the error was
        // > suppressed by the @ operator. As of PHP 8.0.0, it returns
        // > the value E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR |
        // > E_USER_ERROR | E_RECOVERABLE_ERROR | E_PARSE.
        // https://www.php.net/manual/en/language.operators.errorcontrol.php
        if (version_compare(PHP_VERSION, '8.0', 'ge') && $errorReporting === (
            E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR | E_PARSE
        )) {
            return true;
        }

        // PHP 7 or manually disabled case:
        return $errorReporting === 0;
    }

    public function send(EncodedPayload $payload, string $accessToken): ?Response
    {
        if ($this->transmitting()) {
            $response = $this->sender->send($payload, $accessToken);
        } else {
            $response = new Response(0, "Not transmitting (transmitting disabled in configuration)");
            $this->verboseLogger()->warning($response->getInfo());
        }

        if ($this->loggingPayload()) {
            $this->logPayloadLogger()->debug(
                'Sending payload with ' . get_class($this->sender) . ":\n" .
                $payload
            );
        }

        return $response;
    }

    public function sendBatch(array $batch, string $accessToken): ?Response
    {
        if ($this->transmitting()) {
            $this->sender->sendBatch($batch, $accessToken);
            return null;
        } else {
            $response = new Response(0, "Not transmitting (transmitting disabled in configuration)");
            $this->verboseLogger()->warning($response->getInfo());
            return $response;
        }
    }

    public function wait(string $accessToken, $max = 0): void
    {
        $this->verboseLogger()->debug("Sender waiting...");
        $this->sender->wait($accessToken, $max);
    }

    public function handleResponse(Payload $payload, Response $response): void
    {
        if (!is_null($this->responseHandler)) {
            $this->verboseLogger()->debug(
                'Applying custom response handler: ' . get_class($this->responseHandler)
            );
            $this->responseHandler->handleResponse($payload, $response);
        }
    }
}
