<?php

namespace EesyPHP;

use Throwable;
use Exception;

class SentryIntegration {

  /**
   * Sentry DSN
   * @var string|null
   */
  protected static $dsn = null;

  /**
   * Types of PHP error to log in Sentry
   * @see https://www.php.net/manual/fr/errorfunc.constants.php
   * @var array<int>
   */
  protected static $php_error_types;

  /**
   * Initialization
   * @return void
   */
  public static function init() {
    // In phpstan context, do not initialize
    if (defined('__PHPSTAN_RUNNING__') && constant('__PHPSTAN_RUNNING__'))  // @phpstan-ignore-line
      return;
    // Set config default values
    App :: set_default(
      'sentry',
      array(
        'dsn' => null,
        'traces_sample_rate' => 0.2,
        'php_error_types' => array(
          'E_ERROR', 'E_PARSE', 'E_CORE_ERROR', 'E_COMPILE_ERROR', 'E_USER_ERROR',
          'E_RECOVERABLE_ERROR', 'E_DEPRECATED',
        ),
      )
    );
    \Sentry\init([
        'dsn' => App::get('sentry.dsn'),
        'traces_sample_rate' => App::get('sentry.traces_sample_rate', null, 'float'),
    ]);

    \Sentry\configureScope(function (\Sentry\State\Scope $scope): void {
        $scope->setUser([
            'username' => Auth::user()?Auth::user()->username:null,
            'ip_address' => php_sapi_name()=='cli'?null:$_SERVER['REMOTE_ADDR'],
        ]);
    });

    self :: $php_error_types = array();
    foreach(App::get('sentry.php_error_types', null, 'array') as $php_error_type) {
      if (is_string($php_error_type) && defined($php_error_type))
        $php_error_type = constant($php_error_type);
      if (!is_int($php_error_type)) continue;
      if (in_array($php_error_type, self :: $php_error_types)) continue;
      self :: $php_error_types[] = $php_error_type;
    }
    set_error_handler(
      array('EesyPHP\\SentryIntegration', 'on_php_error'),
      E_ALL
    );
  }

  /**
   * Log an exception or a message in Sentry
   * @param string|Throwable $message
   * @param array $extra_args Extra arguments to use to compute message using sprintf
   * @return void
   */
  public static function log($message, ...$extra_args) {
      if (!self :: $dsn) {
          Log :: trace('Sentry DSN not configured, do not log this error');
          return;
      }
      if (is_string($message)) {
          // Extra arguments passed, format message using sprintf
          if ($extra_args) {
            $message = call_user_func_array(
              'sprintf',
              array_merge(array($message), $extra_args)
            );
          }
          Log :: debug('Error logged in Sentry');
          \Sentry\captureMessage($message);
      }
      elseif ($message instanceof Exception) {
          Log :: debug('Exception logged in Sentry');
          \Sentry\captureException($message);
      }
  }

  /**
   * Log a PHP error in Sentry
   * Note: method design to be used as callable by set_error_handler()
   * @param int $errno The error number
   * @param string $errstr The error message
   * @param string $errfile The filename that the error was raised in
   * @param int $errline The line number where the error was raised
   * @return false Return false to let the normal error handler continues.
   */
  public static function on_php_error($errno, $errstr, $errfile, $errline) {
      if (in_array($errno, self :: $php_error_types))
        self :: log(
          "A PHP error occured : [%s] %s\nFile : %s (line : %d)",
          Log :: errno2type($errno), $errstr, $errfile, $errline
        );
      return false;
  }
}

# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab
