<?PHP
/**
 * Debugging and error handling helper for MD.
 */
declare(strict_types = 1);

/**
 * Debugging and error handling class.
 */
final class MDErrorReporter {

    const MD_ERROR_UNKNOWN  = 20;
    const MD_ERROR_CRITICAL = 10;
    const MD_ERROR_KNOWN    = 0;

    /** @var string */
    private string $_context;
    /** @var string */
    private string $_contact_mail_addr;

    /**
     * Categorizes an error to better decide how to act on it.
     *
     * @param Throwable $exception Exception to be categorized.
     *
     * @return integer
     */
    public static function categorizeError(Throwable $exception):int {

        if ($exception instanceof MDpageParameterMissingException
            || $exception instanceof MDpageParameterNotNumericException
            || $exception instanceof MDpageParameterNotFromListException
            || $exception instanceof MDmainEntityNotExistentException
            || $exception instanceof MDmainEntityNotPublicException
            || $exception instanceof MDwriteAccessDeniedException
            || $exception instanceof MDNoUpdateVarSetException
            || $exception instanceof MDDBConnectionImpossible
            || $exception instanceof MDPageNotInAggregatedException
            || $exception instanceof MDWrongFileType
            || $exception instanceof MDTooManyFilesUploadException
            || $exception instanceof MDgenericInvalidInputsException
            || $exception instanceof MDExpectedException
            || $exception instanceof MDMysqliExpectedError
            || $exception instanceof MDcouldNotSetPublic
            || $exception instanceof MDInvalidNodaLink
        ) {
            return self::MD_ERROR_KNOWN;
        }

        return self::MD_ERROR_UNKNOWN;

    }

    /**
     * Function human_filesize translates byte-level filesizes to human readable ones.
     * Thanks to Jeffrey Sambells http://jeffreysambells.com/2012/10/25/human-readable-filesize-php
     *
     * @param integer $bytes    A file size, e.g. returned from filesize().
     * @param integer $decimals Number of decimal digits to allow.
     *
     * @return string
     */
    public function human_filesize(int $bytes, int $decimals = 2):string {

        $size = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
        $factor = \floor((strlen((string)$bytes) - 1) / 3);
        return \sprintf("%.{$decimals}f", $bytes / \pow(1024, $factor)) . $size[$factor];

    }

    /**
     * Function for formatting a section in an error mail.
     *
     * @param string       $headline Headline of the section.
     * @param array<mixed> $toFormat Associative array containing information to
     *                               be formatted.
     * @param integer      $level    Indentation level. Defaults to 0.
     *
     * @return string
     */
    private function _formulateDebugMailSection(string $headline, array $toFormat, int $level = 0):string {

        $msg = "";
        if ($headline !== "") {
            if ($level === 0) {
                $msg .= PHP_EOL . "## " . $headline;
                $msg .= PHP_EOL . "----------------------" . PHP_EOL . PHP_EOL;
            }
            else {
                $msg .= "$headline" . PHP_EOL;
            }
        }

        $longestKey = 0;
        foreach ($toFormat as $key => $value) {
            if (\strlen((string)$key) + (4 * $level) > $longestKey) {
                $longestKey = \strlen((string)$key) + (4 * $level);
            }
        }
        foreach ($toFormat as $key => $value) {
            if (\is_array($value)) {
                $msg .= $this->_formulateDebugMailSection((string)$key, $value, $level + 1);
            }
            else if (\is_object($value)) {
                $msg .= \sprintf("%-{$longestKey}s :: %s", $key, \var_export($value, true)) . PHP_EOL;
            }
            else {
                $msg .= \sprintf("%-{$longestKey}s :: %s", $key, (string)$value) . PHP_EOL;
            }
        }

        return $msg . PHP_EOL;

    }

    /**
     * Gets additional debugging information, e.g. RAM usage.
     *
     * @return array<string|array<string>>
     */
    public function getAdditionalDebuggingInfo():array {

        return [
            "Current memory usage" => $this->human_filesize(\memory_get_usage()),
            "Peak memory usage" => $this->human_filesize(\memory_get_peak_usage()),
            "Included / required files" => \get_included_files(),
            # "Allowed memory usage" => $this->human_filesize((int)ini_get("memory_limit")),
        ];

    }

    /**
     * This function sends a mail to the specified address, containing relevant debug information.
     * .
     *
     * @param string       $to          Recipient mail address.
     * @param string       $subject     Optional subject of the notification. Defaults to "".
     * @param string       $description Optional text of the notification. Defaults to : "".
     * @param array<mixed> $contextInfo Context information that will be displayed in a
     *                                  key-value format at the start of the mail.
     *
     * @return void
     */
    public function sendDebugMail(string $to, string $subject = "", string $description = "", array $contextInfo = []):void {

        $subject = "[{$this->_context}]{$subject}[Controlled error notification]";

        $msg = "
# Automated error message
=========================
    ";

        if ($contextInfo !== []) {
            $msg .= $this->_formulateDebugMailSection("Context information", $contextInfo);
        }

        if ($description !== "") {
            $msg .= "## Description" . PHP_EOL;
            $msg .= "--------------" . PHP_EOL . PHP_EOL;
            $msg .= "$description" . PHP_EOL;
        }

        $msg .= $this->_formulateDebugMailSection("Debug backtrace", \debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT));

        if (PHP_SAPI === 'cli') {
            echo $msg;
            exit;
        }

        // Append the contents of $_SERVER
        if (!empty($_SERVER)) {
            $msg .= $this->_formulateDebugMailSection("The current content of \$_SERVER", $_SERVER);
        }

        // Append the contents of $_SERVER
        if (!empty($_GET)) {
            $msg .= $this->_formulateDebugMailSection("The current content of \$_GET", $_GET);
        }

        // Append the contents of $_SERVER
        if (!empty($_POST)) {
            $msg .= $this->_formulateDebugMailSection("The current content of \$_POST", $_POST);
        }

        // Append the contents of $_SESSION
        if (session_status() === PHP_SESSION_ACTIVE) {
            $msg .= $this->_formulateDebugMailSection("The current content of \$_SESSION", $_SESSION);
        }
        else {
            $msg .= "No active session";
        }

        $msg .= $this->_formulateDebugMailSection("Additional debugging information",
            $this->getAdditionalDebuggingInfo());

        $msg .= PHP_EOL . PHP_EOL;

        $msg = MDMailerHelper::pgp_encrypt($to, $msg);

        $mail = MDMailerHelper::setup_PHPMailer();
        $mail->setFrom($this->_contact_mail_addr, $this->_contact_mail_addr);
        $mail->addAddress($to);
        $mail->addReplyTo(MD_CONF_EMAIL::SMTP_REPLY_TO_ERROR);
        $mail->isHTML(false);
        $mail->Subject = $subject;
        $mail->Body    = "$msg";

        $mail->send();

    }

    /**
     * Wrapper around MDErrorReporter::sendDebugMail that can handle a throwable.
     *
     * @param Throwable $exception Exception to report on.
     * @param string    $recipient Recipient mail address.
     * @param string    $addDesc   Additional descriptive information.
     *
     * @return void
     */
    public function sendErrorReport(Throwable $exception, string $recipient, string $addDesc = ""):void {

        // First, write the error to the general error log. This should happen first,
        // so that it will also work if there is a problem retrieving GPG keys.

        \error_log((string)$exception);

        // Second, a mail is generated and sent.

        $subject = \get_class($exception) . ": " . $exception->getCode();

        $description = $addDesc;

        $additionalContextInfo = [
            "Message" => $exception->getMessage(),
            "File" => $exception->getFile(),
            "Line" => $exception->getLine()
        ];

        $this->sendDebugMail($recipient,
            $subject,
            $description,
            $additionalContextInfo
        );

    }

    /**
     * Constructor.
     *
     * @param string $toolName          Name of the current tool.
     * @param string $contact_mail_addr Mail address for mail's from header.
     *
     * @return void
     */
    public function __construct(string $toolName = "museum-digital (unspecified)", string $contact_mail_addr = "bugs-md@museum-digital.de") {

        $this->_context = $toolName;
        $this->_contact_mail_addr = $contact_mail_addr;

    }
}