<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/29/16
 * Time: 8:03 AM
 */

namespace IMATHUZH\Qfq\Core\Exception;

use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Database\DatabaseQfqSpecific;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnArray;
use IMATHUZH\Qfq\Core\Helper\OnString;
use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\QuickFormQuery;
use IMATHUZH\Qfq\Core\Report\Link;
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Store\T3Info;


/**
 * Class AbstractException
 *
 * Throw with ONE message
 *
 *   throw new \UserFormException('Failed: chmod ....', ERROR_IO_CHMOD);
 *
 * Throw with message for User and message for Support.
 *
 * throw new \UserFormException(  json_encode(
 * [ERROR_MESSAGE_TO_USER => 'Failed: chmod',
 * ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'",
 * ERROR_MESSAGE_HTTP_STATUS => 'HTTP/1.0 409 Bad Request' ]),
 * ERROR_IO_CHMOD);
 *
 * @package qfq
 */
class AbstractException extends \Exception {

    public $messageArray = array();
    public $messageArrayDebug = [EXCEPTION_MESSAGE_DEBUG => ''];

    public $store = null;

#    protected string $file = '';
#    protected int $line = 0;

    protected $httpStatusCode = '400 Bad Request';

    /**
     *
     * @param $msg - "[ mysqli: 1146 ] Table 'dev_data.funnyTableName' doesn't exist"
     * @return string
     */
    private function checkHandleSqlError($arrMsg) {

        $html = '';

        // Check for 'mysqli'
        $msg = $arrMsg[ERROR_MESSAGE_OS] ?? '';
        $arr = explode(' ', $msg);
        if (($arr[1] ?? '') != 'mysqli:') {
            return '';
        }

        // Extract dbName and tablename. String: 'dev_data.funnyTableName'
        $dbTable = OnString::trimQuote($arr[5] ?? '');
        $arr2 = explode('.', $dbTable);
        $dbName = $arr2[0] ?? '';
        $name = $arr2[1] ?? '';

        switch ($arr[2] ?? '') {
            case 1146:
                //  [ mysqli: 1146 ] Table 'dev_data.funnyTableName' doesn't exist
                // Check if missing table is a QFQ table.
                $files = HelperFile::isQfqTable($name, $indexMatch);
                if (false === $files) {
                    return ''; //no QFQ
                }

                // Build a form to let the user choose which tables should be created.
                $dbQfqUpdate = new DatabaseQfqSpecific();
                $html = $dbQfqUpdate->formCreateQfqTable($indexMatch, $files, $arrMsg[ERROR_MESSAGE_DB_INDEX] ?? '');
                break;
            case 1305:
                // [ mysqli: 1305 ] FUNCTION dev_db.QMARK1 does not exist
                $files = HelperFile::isQfqFunction($name, $indexMatch);
                if (false === $files) {
                    return ''; //no QFQ
                }

                // Build a form to let the user choose which tables should be created.
                $dbQfqUpdate = new DatabaseQfqSpecific();
                $html = $dbQfqUpdate->formCreateQfqFunction($indexMatch, $files, $arrMsg[ERROR_MESSAGE_DB_INDEX] ?? '');

                break;
            default:
                break;
        }

        return $html;
    }

    /**
     * $this->getMessage() might give
     *   a) a simple string, or
     *   b) an JSON String.
     *
     * If it is a JSON String: There are 3+1 different messages:
     *   [ERROR_MESSAGE_TO_USER] 'toUser' - shown in the client to the user - no details here!!!
     *   [ERROR_MESSAGE_SUPPORT] 'support' - help for the developer
     *   [ERROR_MESSAGE_OS] 'os' - message from the OS, like 'file not found'
     *
     * Stacktrace, Form, FormElement, Report level, T3 page, T3 tt_content uid, ...
     *
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     */
    public function formatException() {

        $t3Vars = array();
        $arrShow = $this->messageArray;
        $htmlDebug = '';
        $arrDebugShow = array();
        $arrForm = [];

        try {
            // In a very early stage, it might be possible that Store can't be initialized: take care not to use it.
            $store = Store::getInstance();
            $t3Vars = $store->getStore(STORE_TYPO3);
        } catch (\CodeException $e) {
            $store = null;
        } catch (\Exception $exception) {
            $store = null;
        }

        if (empty($t3Vars)) {
            $t3Vars = T3Info::getVars();
        }

        $arrShow[EXCEPTION_TIMESTAMP] = date('Y.m.d H:i:s O');
        $arrShow[EXCEPTION_CODE] = $this->getCode();
        $arrShow[EXCEPTION_UNIQID] = uniqid();

        // Get exception message and if JSON, decode it.
        $msg = $this->getMessage();
        $arrMsg = json_decode($msg, true);
        if ($arrMsg === null) {

            $arrShow[EXCEPTION_MESSAGE] = $msg;
            $arrMsg[ERROR_MESSAGE_TO_USER] = $msg;

        } else {
            $arrShow[EXCEPTION_MESSAGE] = $arrMsg[ERROR_MESSAGE_TO_USER];

            if (isset($arrMsg[ERROR_MESSAGE_HTTP_STATUS])) {
                $this->httpStatusCode = $arrMsg[ERROR_MESSAGE_HTTP_STATUS];
            }
        }

        $arrDebugHidden[EXCEPTION_FILE] = $this->getFile();
        $arrDebugHidden[EXCEPTION_LINE] = $this->getLine();
        $arrDebugHidden[EXCEPTION_CWD] = getcwd();

        $arrTrace = $this->getExtensionTraceAsArray();
        if ($store !== null) {

            $this->messageArrayDebug[EXCEPTION_MESSAGE_DEBUG] = Store::getVar(EXCEPTION_MESSAGE_DEBUG, STORE_SYSTEM);

            $arrDebugShow = array_merge([EXCEPTION_REPORT_FULL_LEVEL => Store::getVar(SYSTEM_REPORT_FULL_LEVEL, STORE_SYSTEM)], $this->messageArrayDebug);

            $arrDebugShow[EXCEPTION_SIP] = $store->getStore(STORE_SIP);
            $arrDebugShow[EXCEPTION_PAGE_ID] = $t3Vars[TYPO3_PAGE_ID] ?? '-';
            $arrDebugShow[EXCEPTION_TT_CONTENT_UID] = $t3Vars[TYPO3_TT_CONTENT_UID] ?? '-';
            $arrDebugShow[EXCEPTION_FE_USER] = $t3Vars[TYPO3_FE_USER] ?? '-';
            $arrDebugShow[EXCEPTION_FE_USER_STORE_USER] = Store::getVar(TYPO3_FE_USER, STORE_USER);

            // Optional existing arrays will be flattened
            $arrDebugShow = OnArray::varExportArray($arrDebugShow);
            $arrDebugHidden = OnArray::varExportArray($arrDebugHidden);

            $arrDebugHidden[EXCEPTION_IP_ADDRESS] = $store->getVar(CLIENT_REMOTE_ADDRESS, STORE_CLIENT);
            // No need for this information:
            // $arrDebugHidden[EXCEPTION_HTTP_USER_AGENT] = $store->getVar(CLIENT_HTTP_USER_AGENT, STORE_CLIENT, SANITIZE_ALLOW_ALLBUT);
            // $arrDebugHidden[EXCEPTION_QFQ_COOKIE] = $store->getVar(CLIENT_COOKIE_QFQ, STORE_CLIENT, SANITIZE_ALLOW_ALNUMX);

            // Debug Information
            if (Support::findInSet(SYSTEM_SHOW_DEBUG_INFO_YES, $store->getVar(SYSTEM_SHOW_DEBUG_INFO, STORE_SYSTEM))) {

                // In case there is a 'form' name given in SIP, we probably have a problem in a form and a direct link to
                // edit the broken form will be helpful.
                $storeSystem = $store->getStore(STORE_SYSTEM);
                if (!empty($storeSystem[SYSTEM_FORM])) {
                    $arrForm['Edit'] = $this->buildFormLink($storeSystem);
                    $arrForm['FE column'] = Store::getVar(SYSTEM_FORM_ELEMENT_COLUMN, STORE_SYSTEM);
                }

                //  Check if the 'developer message shouldn't be sanitized
                $developerRaw = null;
                if (($arrMsg[ERROR_MESSAGE_TO_DEVELOPER_SANITIZE] ?? true) == false) {
                    $developerRaw = $arrMsg[ERROR_MESSAGE_TO_DEVELOPER] ?? '';
                }
                unset($arrMsg[ERROR_MESSAGE_TO_DEVELOPER_SANITIZE]);

                $arrMerged = OnArray::htmlentitiesOnArray(array_merge($arrMsg, $arrDebugShow));
                // Restore raw developer message
                if ($developerRaw !== null) {
                    $arrMerged[ERROR_MESSAGE_TO_DEVELOPER] = $developerRaw;
                }

                $arrMerged[EXCEPTION_DB_UPDATE_FORM] = $this->checkHandleSqlError($arrMsg);

                if (!empty($os = $arrMerged[ERROR_MESSAGE_OS] ?? '')) {
                    // [ mysqli: 1146 ] Table 'qfq_db.UNKNOWN_TABLE' doesn't exist
                    $before = $this->getTableToken(html_entity_decode($arrMerged[ERROR_MESSAGE_OS], ENT_QUOTES));
                    $arrMerged[EXCEPTION_SQL_FINAL] = $this->sqlHighlightError($arrMerged[ERROR_MESSAGE_OS], 'mysqli: 1146', $arrMerged[EXCEPTION_SQL_FINAL], $before, "' doesn't exist");
                    $arrMerged[EXCEPTION_SQL_FINAL] = $this->sqlHighlightError($arrMerged[ERROR_MESSAGE_OS], 'mysqli: 1064', $arrMerged[EXCEPTION_SQL_FINAL], "the right syntax to use near '", "' at line [0-9]*$");
                    // [ mysqli: 1054 ] Unknown column "noPsp.pspElement' in 'field list" | "... in 'order clause'"
                    $arrMerged[EXCEPTION_SQL_FINAL] = $this->sqlHighlightError($arrMerged[ERROR_MESSAGE_OS], 'mysqli: 1054', $arrMerged[EXCEPTION_SQL_FINAL], "Unknown column '", "' in '");
                }

                foreach ($arrMerged as $key => $value) {
                    $arrMerged[$key] = str_replace("\n", "<br>", $arrMerged[$key]);
                }

                // Render inline report editor
                $arrMerged[ERROR_MESSAGE_TO_DEVELOPER] = $arrMerged[ERROR_MESSAGE_TO_DEVELOPER] ?? '';
                try {
                    $arrMerged[ERROR_MESSAGE_TO_DEVELOPER] = QuickFormQuery::buildInlineReport(\UserReportException::$report_uid,
                            \UserReportException::$report_pathFileName, \UserReportException::$report_db) . $arrMerged[ERROR_MESSAGE_TO_DEVELOPER];
                } catch (\Error|\Exception $e) {
                    $arrMerged[ERROR_MESSAGE_TO_DEVELOPER] .= "<br>(inline report editor not available)";
                }

                $htmlDebug = '<div class="qfq-debug-detail qfq-alert-hidden">' . OnArray::arrayToHtmlTable(
                        array_merge($arrForm, $arrMerged),
                        'Debug', EXCEPTION_TABLE_CLASS);

                $arrDebugHiddenClean = OnArray::htmlentitiesOnArray($arrDebugHidden);
                $arrDebugHiddenClean[EXCEPTION_STACKTRACE] = implode('<br>', $arrTrace);
//                $arrDebugHiddenClean[EXCEPTION_EDIT_FORM] = implode($arrTrace, '<br>');
                $hidden = OnArray::arrayToHtmlTable($arrDebugHiddenClean, 'Details', EXCEPTION_TABLE_CLASS);

                // Show / hide with just CSS: http://jsfiddle.net/t5Nf8/1/
                $htmlDebug .= "<hr>$hidden</div>";
            }
        }


        $qfqLog = Path::absoluteQfqLogFile();
        $arrDebugHidden[EXCEPTION_STACKTRACE] = PHP_EOL . implode(PHP_EOL, $arrTrace);
        $arrLogAll = array_merge($arrMsg, $arrShow, $arrDebugShow, $arrDebugHidden);
        $logAll = OnArray::arrayToLog($arrLogAll);
        if (!defined('PHPUNIT_QFQ')) {
            Logger::logMessage($logAll, $qfqLog);
        }

        // Sanitize any HTML content.
        $arrShow = OnArray::htmlentitiesOnArray($arrShow);

        return $this->formatMessageUser($arrShow) . $htmlDebug;

    }

    /**
     * @return string
     */
    public function getHttpStatus() {
        return $this->httpStatusCode;
    }

    /**
     * Extract 'beforeMatch', incl. dynamic db name as token to do underlining later.
     * E.g.:  "[ mysqli: 1146 ] Table 'qfq_db.UNKNOWN_TABLE' doesn't exist"
     * return: "Table 'qfq_db."
     *
     * @param $os
     * @return string
     */
    private function getTableToken($os) {
        $subject = "Table '.*' ";
        $arr = preg_match("/$subject/", $os, $matches);
        $arr = explode('.', $matches[0] ?? '');
        return ($arr[0] ?? '') . '.';
    }

    /**
     * @param $os
     * @param $code
     * @param $sql
     * @param $before
     * @param $after
     * @return string
     */
    private function sqlHighlightError($os, $code, $sql, $before, $after) {

        // In case the sql was truncated replace ...&#039; with &#039; so it can still find a match in sql.
        // Example:
        // before = near &#039;FROM (Request AS re, Person AS pCreator) LEFT JOIN Grp AS grStatus ON re.grId...&#039; at line 1
        // after = near &#039;FROM (Request AS re, Person AS pCreator) LEFT JOIN Grp AS grStatus ON re.grId&#039; at line 1
        $os = preg_replace("/\.\.\.&#039;/", "&#039;", $os);

        $beforeMatch = htmlentities($before, ENT_QUOTES);
        $afterMatch = htmlentities($after, ENT_QUOTES);
        if (preg_match("/$code.*$beforeMatch.*$afterMatch/", $os)) {
            $arr = explode("$beforeMatch", $os, 2);
            $match = $arr[1] ?? '';
            $match = preg_split("/$afterMatch/", $match)[0];

            if (!empty($match)) {
                $splitSql = explode($match, $sql);
                $match = Support::wrapTag('<span class="qfq-wavy-underline">', $match);
                $highlightedSql = implode($match, $splitSql);
                $sql = $highlightedSql;
            }
        }
        return $sql;
    }

    /**
     * @return array
     */
    private function getExtensionTraceAsArray() {

        $trace = $this->getTraceAsString();
        $arrTrace = explode(PHP_EOL, $trace);

        return OnArray::filterValueSubstring($arrTrace, Path::APP_TO_EXT);
    }

    /**
     * @param $arrShow
     * @return string
     */
    private function formatMessageUser($arrShow) {

        $html = '<span class="qfq-alert-timestamp">' . $arrShow[EXCEPTION_TIMESTAMP] . '</span><span class="qfq-alert-reference">' . $arrShow[EXCEPTION_UNIQID] . '</span>';
        $html .= '<p>' . $arrShow[EXCEPTION_MESSAGE] . '</p>';

        return $html;
    }

    /**
     * Build a FormEditor link to the broken form.
     * @param $storeSystem
     * @return string
     */
    private function buildFormLink($storeSystem) {

        $linkForm = '';
        $linkFormElement = '';
        try {
            $db = new Database();
            $sql = "SELECT `id` FROM `Form` WHERE `name`='" . $storeSystem[SYSTEM_FORM] . "'";

            $r = $db->sql($sql, ROW_EXPECT_0_1);
            if (!is_numeric($r[F_ID])) {
                return '';
            }

            $sip = new Sip();
            $link = new Link($sip);

            // Link to 'Form'
            $linkForm = $link->renderLink(TOKEN_SIP . '|' . TOKEN_BOOTSTRAP_BUTTON . '|' . TOKEN_PAGE . ':' .
                $storeSystem[SYSTEM_EDIT_FORM_PAGE] . '&' . CLIENT_FORM . '=' . FORM_NAME_FORM . '&' .
                CLIENT_RECORD_ID . '=' . $r[F_ID] . '|' . TOKEN_TEXT . ':' . $storeSystem[SYSTEM_FORM]);

            // Link to 'FormElement'
            if (!empty($storeSystem[SYSTEM_FORM_ELEMENT_ID])) {
                $linkFormElement = $link->renderLink(TOKEN_SIP . '|' . TOKEN_BOOTSTRAP_BUTTON . '|' . TOKEN_PAGE .
                    ':' . $storeSystem[SYSTEM_EDIT_FORM_PAGE] . '&' . CLIENT_FORM . '=' . FORM_NAME_FORM_ELEMENT . '&' .
                    CLIENT_RECORD_ID . '=' . $storeSystem[SYSTEM_FORM_ELEMENT_ID] . '|' .
                    TOKEN_TEXT . ':' . $storeSystem[SYSTEM_FORM_ELEMENT]);
            }

        } catch (\exception $e) {
            // none, should rise up
            return "Error build direct 'Form-Edit-Link'";
        }

        return 'Form: ' . $linkForm . '&nbsp;&nbsp;  FormElement: ' . $linkFormElement;
    }
}