<?php

namespace Velis;

use ErrorException;
use Phalcon\Http\Message\ResponseStatusCodeInterface;
use Throwable;
use Velis\Debugger\ErrorReporter;
use Velis\Http\Request;
use Velis\Http\Response;
use Velis\Log\LoggerInterface;
use Velis\Mail\Mail;
use Velis\Model\DataObject\NoColumnsException;

/**
 * Debugging functions class
 * @author Olek Procki
 */
class Debug
{
    const MODE_RETURN_HTML = 'ReturnHtml';
    const MODE_RETURN      = true;
    const MODE_OUTPUT      = false;


    /**
     * Returns or put on output expression dump
     *
     * @param mixed       $expression expression to be dumped
     * @param bool|string $mode       if this parameter is set to TRUE, dump() will return its output, instead of printing it (which it does by default).
     *
     * @return string|null
     */
    public static function dump($expression, $mode = self::MODE_OUTPUT)
    {
        $str = print_r($expression, true);

        if (!$_SERVER['CLI_MODE'] || $mode === self::MODE_RETURN_HTML) {
            $search = [
                '>',
                '<',
                '[',
                ']',
                "Object\n",
                "Array\n",
                '=>',
                "  ",
                ")\n",
            ];

            $replace = [
                '&gt;',
                '&lt;',
                '&#91;<span style="color: red;">',
                '</span>&#93;',
                "<span style='color: #FF8000;'>Object</span>\n",
                "<span style='color: blue;'>Array</span>\n",
                '<span style="color: #009900;">=&gt;</span>',
                "    ",
                ")",
            ];

            $str = str_ireplace($search, $replace, $str);

            $str = "<pre style='font-family:Open Sans; font-size:10px; border: none; background: none;'>" . $str  . "</pre>\n";
        }

        if ($mode != self::MODE_OUTPUT) {
            return $str;
        } else {
            echo $str;
        }
    }


    /**
     * Dumps variable & stops application
     *
     * @param mixed $expression
     * @return string
     */
    public static function stop($expression = null)
    {
        self::dump($expression);
        exit;
    }

    /**
     * @deprecated use ErrorReporter::log() instead
     */
    public static function logError(Throwable $e, bool $skipLegacy = false): void
    {
        $errorReporter = App::getService(ErrorReporter::class);
        $errorReporter->log($e, $skipLegacy);
    }

    public static function exceptionHandler(Throwable $e): void
    {
        self::logError($e);
        self::showErrorResponsePage($e);
    }

    /**
     * @throws NoColumnsException
     * @deprecated use ErrorReporter::report() instead
     */
    public static function reportException(Throwable $e): void
    {
        $errorReporter = App::getService(ErrorReporter::class);
        $errorReporter->report($e);
    }

    /**
     * Send notification error
     * @param \Velis\Notification\Log $row
     * @param \Exception $e
     */
    public static function reportMailException($row, $e)
    {
        $msg  = "* Nadawca: " . $row['sender'] . "\n";
        if ($row['recipients_cc']) {
            $msg .= "* Adresaci cc: " . $row['recipients_cc'] . "\n";
        }
        if ($row['recipients_bcc']) {
            $msg .= "* Adresaci bcc: " . $row['recipients_bcc'] . "\n";
        }

        $msg .= "* Temat maila: " . $row['subject'] . "\n";
        $msg .= "* Data dodania do kolejk: " . substr($row['send_date'], 0, 19) . "\n";
        $msg .= "* Treść ostatniego komunikatu o błędzie: " . $e->getMessage() . "\n";

        $htmlMsg = '<ul>';
        $htmlMsg .= "<li> Nadawca: <code>" . $row['sender'] . "</code></li>";
        if ($row['recipients_cc']) {
            $htmlMsg .= "<li> Adresaci cc: <code>" . $row['recipients_cc'] . "</code></li>";
        }
        if ($row['recipients_bcc']) {
            $htmlMsg .= "<li> Adresaci bcc: <code>" . $row['recipients_bcc'] . "</code></li>";
        }

        $htmlMsg .= "<li> Temat maila: <code>" . $row['subject'] . "</code></li>";
        $htmlMsg .= "<li> Data dodania do kolejk: <code>" . substr($row['send_date'], 0, 19) . "</code></ul>";
        $htmlMsg .= "<li> Treść ostatniego komunikatu o błędzie: <code>" . $e->getMessage() . "</code></ul>";
        if (App::$config->notifications->error) {
            $account = Mail::getErrorNotificationAccount();
            $from = App::$config->notifications->error->email_address;
        } else {
            $from = App::$config->notifications->emailFrom;
        }

        $mail = new Mail($account);
        $mail->setEncoding('utf-8');

        $params['body_text'] = $msg;
        $params['body_html'] = $htmlMsg;

        $mail->setContents($params)
             ->setSubject('Przekroczono max ilość prób wysyłki [' . App::$config->notifications->nameFrom . ']')
             ->setFrom($from, 'Error ' . App::$config->settings->instanceAcro)
             ->addTo(App::$config->errorReporting->recipient);

        $mail->send();
    }

    /**
     * Reports xss detection to external file
     */
    public static function reportXSS(): void
    {
        /** @var LoggerInterface $logger */
        $logger = App::getService('logger.xss');

        $msg = "\n* ID SESJI: " . session_id() . "\n";
        $msg .= "* ID UŻYTKOWNIKA: " . App::$user->id() . "\n";
        $userRolesStr = implode(', ', array_keys(App::$session->userRoles));
        $msg .= "* UŻYTKOWNIK: " . App::$user->getFullName() . " ($userRolesStr)\n";
        $msg .= "* ADRES IP: " . $_SERVER['REMOTE_ADDR'] . "\n";
        $msg .= "* HOST: " . $_SERVER['REMOTE_HOST'] . "\n";
        $msg .= "* PORT: " . $_SERVER['REMOTE_PORT'] . "\n";
        $msg .= "* REQUEST URI: " . $_SERVER['REQUEST_URI'] . "\n";
        $msg .= "* PRZEGLĄDARKA: " . $_SERVER['HTTP_USER_AGENT'] . "\n\n";
        $msg .= print_r(App::$registry['filter']->getRawCopy(), true);

        $logger->error('!XSS DETECTED!' . $msg . "\n" . str_repeat('-', 160));
    }

    /**
     * Checks filter for tags
     */
    public static function checkXSS()
    {
        array_walk_recursive(App::$registry['filter'], array(self, 'checkField'));
    }



    /**
     * Checks field for html tags
     *
     * @param mixed $item
     * @param string $key
     */
    public static function checkField(&$item, $key)
    {
        $filter = App::$registry['filter'];

        if (!$filter::$_xssFiltered) {
            if (!is_array($item) && !in_array($key, $filter::$_unfiltered)) {
                if ($item != strip_tags($item)) {
                    self::reportXSS();
                    $filter::$_xssFiltered = true;
                }
            }
        }
    }


    /**
     * Clear php error logs after $days
     */
    public static function clearLogs()
    {
        $days = App::$config->settings->clearLogs;

        if ($days) {
            $path = LOG_PATH . '{php,db}/';
            $files = glob($path . '*.log', GLOB_BRACE);

            if (count($files) > 0) {
                foreach ($files as $filename) {
                    if (floor((time() - filemtime($filename)) / (3600 * 24)) > $days) {
                        @unlink($filename);
                    }
                }
            }
        }
    }


    /**
     * Shutdown handler for PHP errors
     */
    public static function shutdownHandler()
    {
        if ($e = error_get_last()) {
            if (is_int(stripos($e['message'], 'Allowed memory size of'))) {
                // Unlock previously allocated emergency memory
                unset($GLOBALS['reserved_memory']);
            }

            if (in_array($e['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
                $errorInfo = array();
                if (App::$registry['filter']) {
                    $errorInfo['filter'] = App::$registry['filter']->getRawCopy();
                }

                if (session_id() && App::$session->settings) {
                    $errorInfo['settings'] = App::$session->settings;
                }

                $errorInfo['server'] = Arrays::extractFields(
                    $_SERVER,
                    array(
                        'REMOTE_ADDR',
                        'REMOTE_HOST',
                        'REMOTE_ADDR',
                        'REQUEST_URI',
                        'HTTP_USER_AGENT',
                        'HTTP_REFERER',
                    )
                );

                if (App::$user && App::$user->isLogged()) {
                    $errorInfo['session_id'] = session_id();
                    $errorInfo['user']       = App::$user->getFullName() . ' (' . App::$user->id() . ')';
                    $errorInfo['roles']      = implode(', ', array_keys(App::$session->userRoles));
                }

                // Legacy method, to be removed in the future
                error_log(
                    "LAST ERROR INFO: " . print_r($errorInfo, true)
                );

                $exception = new ErrorException(
                    message: $e['message'] ?? 'Unknown error',
                    code: 0,
                    severity: $e['type'] ?? E_ERROR,
                    filename: $e['file'] ?? 'unknown',
                    line: $e['line'] ?? 0
                );
                self::logError($exception, skipLegacy: true);
            }
        }
    }

    public static function showErrorResponsePage(Throwable $e): void
    {
        /** @var Response $response */
        $response = App::$di['response'];

        $response->setStatusCode(ResponseStatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);

        $data = [];
        if (App::$config->settings->debugMode) {
            $data['debug'] = [
                'message' => $e->getMessage(),
                'file' => $e->getFile(),
                'line' => $e->getLine(),
                'trace' => $e->getTrace(),
                'previous' => $e->getPrevious(),
                'queries' => App::$di['db']->getExecutedQueries(),
            ];
        }

        /** @var Request $request */
        $request = App::$di['request'];
        if ($request->acceptJson()) {
            $response->setJsonContent([
                    'message' => 'Internal server error',
                    'correlationId' => App::getInstance()->getCorrelationId(),
                ] + $data);
        } else {
            include APP_PATH . 'view/layout/startup-error.php';
        }
        $response->sendForce();
    }
}
