<?php
namespace BitFire;

use function ThreadFin\dbg;
use function ThreadFin\trace;
use function ThreadFin\debug;
use function ThreadFin\emerg;
use function ThreadFin\get_hidden_file;
use function ThreadFin\path_to_slug;

use BitFire\Config as CFG;
use ThreadFin\CacheStorage;

const ERRORS_ONLY = E_WARNING | E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_COMPILE_WARNING | E_RECOVERABLE_ERROR;
// map these error numbers to stat counters. GEO location counters end at 512...
const ERROR_MAP = [E_WARNING => 513, E_ERROR => 514, E_PARSE => 515, E_CORE_ERROR => 516, E_COMPILE_ERROR => 517, E_COMPILE_WARNING => 518, E_RECOVERABLE_ERROR => 519];

const ERROR_NAME = [E_WARNING => 'warning', E_ERROR => 'error', E_PARSE => 'parse', E_CORE_ERROR => 'core_error', E_COMPILE_ERROR => 'compile_error', E_COMPILE_WARNING => 'compile_warn', E_RECOVERABLE_ERROR => 'recoverable_error'];

define('WAF_ROOT2', defined('WAF_ROOT') ? WAF_ROOT : __DIR__ . DIRECTORY_SEPARATOR);
define('INFO2', defined('INFO') ? INFO : 'https://info.bitfire.co/');

// include our mapping for plugin slugs to IDS (used in the error counter)
$f = __DIR__ . DIRECTORY_SEPARATOR . 'plugin_mapping.php';
if (file_exists($f)) {
    include_once $f;
}

/**
 * report an errors in the software so we can include a fix in the next release
 * This entire function is a side-effect or sending off errors
 */
function report_error(string $url, string $error_key) {
    $key = substr($error_key, 0, 8);

    // don't report errors if error reporting is off...
    if (class_exists('\BitFire\Config') && CFG::enabled('send_errors', false) == false) {
        return;
    }
    // don't spam out error reports...
    if (class_exists('\ThreadFin\CacheStorage')) {
        $have_sent = \ThreadFin\CacheStorage::get_instance()->load_data("error_sent:$key", false, CACHE_LOW);
        if ($have_sent) {
            return;
        }
    }
    // trivial curl fallback - 0 dependencies
    if (function_exists('curl_init')) {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);    // Capture output into a variable
        curl_setopt($ch, CURLOPT_TIMEOUT_MS, 500);         // Set a timeout to avoid hanging
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, 500);  // Set a timeout to avoid hanging
        curl_setopt($ch, CURLOPT_SSL_VERIFYSTATUS, false); // no OCSP stapling, VERIFYPEER and VERIFYHOST default to true
        curl_exec($ch);
        curl_close($ch);
    }
    // no curl, lets try fopen
    else if (ini_get("allow_url_fopen") == 1) {
        // set a timeout in case the server is unreachable...
        $ctx = stream_context_create([
            'http' => ['method' => 'GET', 'timeout' => 1],
            'ssl'  => ['verify_peer_name' => true],
        ]);
        file_get_contents($url, false, $ctx);
    }

    // flag that we have sent this error report...
    if (class_exists('\ThreadFin\CacheStorage')) {
        \ThreadFin\CacheStorage::get_instance()->save_data("error_sent:$key", "true", 300, CACHE_HIGH);
    }
}


/**
 * add a low counter and a high counter together
 * @param int $a 
 * @param int $b 
 * @return int  - return the combined result
 */
function error_add_ints(int $a, int $b) {
    $a_low = $a & 0xFFFF;
    $a_high = ($a >> 16) & 0xFFFF;
    $b_low = $b & 0xFFFF;
    $b_high = ($b >> 16) & 0xFFFF;

    $sum_low = ($a_low + $b_low) & 0xFFFF;
    $sum_high = ($a_high + $b_high) & 0xFFFF;

    return ($sum_high << 16) | $sum_low;
}

/**
 * add a single value to either low or high counter
 */
function error_inc(bool $is_warning, int $counter, int $amount = 1) : int {
    $warn =  $counter         & 0xFFFF;                // low 16
    $errs = ($counter >> 16)  & 0xFFFF;                // high 16

    if ($is_warning) {
        $warn = ($warn + $amount) & 0xFFFF;              // keep to 16 bits
    } else {
        $errs = ($errs + $amount) & 0xFFFF;
    }

    return ($errs << 16) | $warn;
}

/**
 * Count all errors that occur in all plugins / themes
 * @return array - return the error counter
 */
function error_counter(int $level = -1, string $file = '') : array {
    static $cached = [];

    if ($level == -1 || !defined('BITFIRE_PLUGIN_MAP')) {
        return $cached;
    }

    if (function_exists('\ThreadFin\path_to_slug')) {
        $slug = path_to_slug($file);
        $plugin_id = BITFIRE_PLUGIN_MAP[$slug] ?? 64;
        $val  = $cached[$plugin_id] ?? 0;

        $cached[$plugin_id] = error_inc(($level == E_WARNING), $val);
    }
    return $cached;
}


/**
 * set the default error handler while running bitfire 
 * @return bool 
 */
function on_err($errno, $errstr, $err_file, $err_line): bool {
    static $seen_errors = [];
    static $to_send = [];
    static $ctr = 0;


    // don't do anything unless we are handling a real error
    if ($errno > 0) {
        // too many errors, stop processing
        if ($ctr++ > 50) {
            return false;
        }
        // count the error
        error_counter($errno, $err_file);
        if (class_exists("\BitFire\Config")) {
            if (CFG::int("log_errors") > time() && function_exists("\ThreadFin\get_hidden_file")) {
                $line = date(DATE_ATOM) . " " . strtoupper(ERROR_NAME[$errno] ?? 'unknown') . " in $err_file:$err_line\n -> $errstr\n";
                file_put_contents(get_hidden_file("site_errors.log"), $line, FILE_APPEND | LOCK_EX);
            }
        }
    }

    // don't track non-bitfire errors
    if (strpos($err_file, 'bitfire') === 0) {
        return false;
    }

    // don't repeat yourself in case a single request generated errors in a loop 
    $error_key = md5($errno . '|' . $err_file . '|' . $err_line);
    if (isset($seen_errors[$error_key])) {
        return false; // Already handled this exact error
    }
    $seen_errors[$error_key] = true;


    // this is called once in the shutdown handler, that's when we will update everything
    if ($errno < -99) {
        $c = count($to_send);

        array_walk($to_send, function ($data, $error_key) {
            if (!has_been_sent($data)) {
                if (function_exists('\ThreadFin\debug')) {
                    $data['debug'] = \ThreadFin\debug(null);
                    $data['trace'] = \ThreadFin\trace(null);
                }

                $msg = sprintf("host=%s&file=%s&line=%s&errno=%s&errstr=%s&phpver=%s&&ver=%s", 
                    urlencode($_SERVER['HTTP_HOST'] ?? 'local'),
                    urlencode($data['err_file']),
                    urlencode($data['err_line']),
                    urlencode($data['errno']),
                    urlencode($data['errstr']),
                    urlencode($data['php_ver']),
                    urlencode($data['ver'])
                );

                if (stripos($msg, 'error_handler') !== false) {
                    return;
                }

                $len1 = strlen($msg);
                $bt = urlencode(json_encode($data['bt'] ?? []) ?? []);
                $len2 = strlen($bt);
                if ($len1 + $len2 < 2048) {
                    $msg .= "&bt=$bt";
                }

                $url = INFO2 . "err.php?ver=" . (defined('BITFIRE_VER') ? BITFIRE_VER : 'unknown') . "&$msg";
                report_error($url, $error_key);

            }
        });
        return true;
    }

    $data = [
        'ver'       => defined('BITFIRE_VER') ? BITFIRE_VER : 'unknown',
        'type'      => defined('BITFIRE_TYPE') ? BITFIRE_TYPE : 'unknown',
        'errno'     => $errno,
        'errstr'    => $errstr,
        'err_file'  => $err_file,
        'err_line'  => $err_line,
        'php_ver'   => phpversion(),
    ];

    if (class_exists('\BitFire\Config') && \BitFire\Config::enabled('send_errors', true)) {
        // for privacy, don't include any args or full path info
        $data['bt'] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
        foreach ($data['bt'] as $i => $frame) {
            $data['bt'][$i]['file'] = basename($frame['file'] ?? 'unknown');
        }
    }

    $to_send[] = $data;
    return true;
}


/**
 * Return true if this error was already sent, else record and return false.
 * Small and dependency free, uses a newline-separated signature file.
 */
function has_been_sent(array $data) : bool {
    static $seen = null;

    // derive a stable signature from the essentials
    $err_file = (string)($data['err_file'] ?? '');
    $err_line = (int)($data['err_line'] ?? 0);
    $sig      = sha1($err_file . ':' . $err_line);

    // fail closed to not send the error
    if (!function_exists('\BitFire\get_hidden_file')) {
        return true;
    }
    // choose a portable path, override with BF_ERROR_SENT_FILE if defined
    $path = get_hidden_file("error_sent.txt");

    // one time load per request
    if ($seen === null) {
        if (is_file($path) && is_readable($path)) {
            $tmp = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
            $seen = array_flip($tmp);
        }
    }

    if (isset($seen[$sig])) {
        return true; // already recorded
    }

    $wrote = file_put_contents($path, $sig . "\n", FILE_APPEND | LOCK_EX);
    $seen = [];

    // if we successfully wrote the line, return that the error has not been seen before
    // on error return true - since we cant update the error log - never send it
    $r = ($wrote == false) ? true : false; // first time seen
    return $r;
}

// capture any bitfire fatal errors
// send any errors that have been queued after the page has been served
register_shutdown_function(function () {

    $s1 = hrtime(true);
    $GLOBALS['bf_t1'] = ($GLOBALS['bf_t1']??0) + ((hrtime(true) - $s1) / 1e+6);
    if (function_exists('ThreadFin\debug')) {
       debug('complete [%.2fms] [%s]', ($GLOBALS['bf_t1']), trace());
    }

    $e = error_get_last();
    // if last error was from bitfire, log it
    if (is_array($e) &&
    in_array($e['type']??-1, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR]) &&
    stripos($e['file'] ?? '', 'bitfire') > 0) {
        $e['ver'] = defined('BITFIRE_VER') ? BITFIRE_VER : 'unknown';
        $e['e_type'] = 'FATAL';
        $e['ref_id'] = crc32(uniqid('', true));
        $e['php_ver'] = phpversion();
        $e['server_id'] = ($_SERVER['HTTP_HOST'] ?? 'unknown') . ($_SERVER['REQUEST_URI'] ?? '/na');



        $encoded = array_map(function ($k, $v) {
            return "$k=" . urlencode($v);
        } , array_keys($e), array_values($e));

        $url_params = join('&', $encoded);
        // don't allow endless loops
        if (stripos($url_params, 'error_handler') !== false) {
            return;
        }


        $error_key = md5($e['type'] . '|' . $e['file'] . '|' . $e['line']);
        report_error(INFO2 . 'err.php?' . $url_params, $error_key);
        echo "<h1>Fatal Error Detected.</h1><p>please contact support - support@bitfire.co</p><p>Reference: {$e['ref_id']}</p><p>{$e['message']}</p>\n";//, {$e['file']}:{$e['line']}</p>\n";

        require_once WAF_ROOT2 . "src/server.php";
        $err_counter = get_hidden_file('err_count');

        if (!file_exists($err_counter) || filemtime($err_counter) < time() - 1600) {
            file_put_contents($err_counter, 1);
        }
        else {
            $count = file_get_contents($err_counter);
            if ($count < 5) {
                file_put_contents($err_counter, $count + 1);
            }
            // if we have received 5 fatal errors from humans in the last 26 min, disable bitfire
            else {
                if (function_exists('BitFireSvr\update_ini_value') && class_exists('\BitFire\Request')) {
                    $i = BitFire::get_instance();
                    if (!empty($i)) {
                        if (!empty($i->agent)) {
                            if (! $i->agent->bot) {
                                \BitFireSvr\update_ini_value('bitfire_enabled', 'false')->run();
                            }
                        }
                    }
                }
            }
        }
    }
    // make sure data is always logged
    if (class_exists('\BitFire\Config') && Config::enabled(CONFIG_ENABLED)) {
        log_it();
    }

    // try and disconnect the client before we write the log, or send any errors...
    if (function_exists('fastcgi_finish_request')) {
        fastcgi_finish_request();
    }


    // send any errors that have been queued after the page has been served
    on_err(-100, "", "", 0);
});


// capture any bitfire errors
$error_handler = set_error_handler('\BitFire\on_err', ERRORS_ONLY);
