<?php

/**
 * Class represents records from table error_log
 * {autogenerated}
 * @property int $log_id PK
 * @property int|null $level
 * @property string|null $channel
 * @property int|null $user_id FK user
 * @property datetime $time
 * @property string|null $url
 * @property string|null $remote_addr
 * @property string|null $referrer
 * @property string|null $error
 * @property string|null $trace
 * {/autogenerated}
 */
class ErrorLog extends Am_Record
{
}

/** @extends Am_Table<ErrorLog> */
class ErrorLogTable extends Am_Table implements \Monolog\Handler\HandlerInterface
{
    protected $_key = 'log_id';
    protected $_table = '?_error_log';
    protected $_recordClass = 'ErrorLog';

    // monolog variables
    protected $level = 200; // INFO level. Debug is not logged to table by default
    protected $bubble = true;
    protected $formatter;
    protected $processors = [];

    function clearOld($date)
    {
        $this->_db->query("DELETE FROM ?_error_log WHERE `time` < ? ", $date . ' 00:00:00');
    }

    function clearOldDebug($date)
    {
        $this->_db->query(
            "DELETE FROM ?_error_log WHERE level=? AND `time` < ?;",
            200,
            $date . ' 00:00:00'
        );
    }

    static function backtraceToString(array $trace)
    {
        $out = "";
        foreach ($trace as $t) {
            if (isset($t['class'])) {
                $out .= $t['class'] . $t['type'] . $t['function'];
            } else {
                $out .= $t['function'];
            }
            if (!empty($t['file'])) {
                $file = str_replace(@ROOT_DIR . DIRECTORY_SEPARATOR, '', $t['file']);
                $out .= " [ $file";
                if (!empty($t['line']))
                    $out .= " : {$t['line']}";
                $out .= " ]";
            }
            $out .= "\n";
        }
        return $out;
    }

    function logException($e)
    {
        $error = $e->getMessage();
        if (defined('AM_TEST') && AM_TEST)
            return false;
        $values = [
            'time' => $this->getDi()->sqlDateTime,
            'user_id' => $this->getDi()->auth->getUserId(),
            'remote_addr' => @$_SERVER['REMOTE_ADDR'],
            'url' => htmlentities(@$_SERVER['REQUEST_URI']),
            'referrer' => htmlentities(@$_SERVER['HTTP_REFERER']),
            'error' => $error,
            'trace' => "Exception " . get_class($e) . PHP_EOL . self::backtraceToString($e->getTrace()),
            'level' =>       400, // error by default
            'channel' =>     'amember-core',
        ];
        if ($e instanceof Am_Exception_Db) {
            @$this->insert($values, false);
        } else {
            $this->insert($values, false);
        }
    }

    /**
     * Log a message
     * @param string $error
     */
    function log($error)
    {
        if ($error instanceof Exception)
            return $this->logException($error);
        if (defined('AM_TEST') && AM_TEST)
            return false;
        if (!$this->getDi()->getService('db') || !@$this->getDi()->db || !$this->getDi()->db->_isConnected())
            return;
        $trace = debug_backtrace(false);
        array_shift($trace); // remove ErrorLog->log entry
        array_shift($trace); // remove ErrorLog->log entry
        try { // necessary to avoid errors if init is not finished
            $user_id = $this->getDi()->auth->getUserId();
        } catch (Exception $e) {
            $user_id = null;
        }
        $values = [
            'time' => $this->getDi()->sqlDateTime,
            'user_id' => $user_id,
            'remote_addr' => @$_SERVER['REMOTE_ADDR'],
            'url' => htmlentities(@$_SERVER['REQUEST_URI']),
            'referrer' => htmlentities(@$_SERVER['HTTP_REFERER']),
            'error' => $error,
            'trace' => self::backtraceToString($trace),
            'level' =>       400, // error by default
            'channel' =>     'amember-core',
        ];
        try {
            $this->insert($values, false);
        } catch (Am_Exception_Db $e) { // do not break logging until admin-upgrade-db to v.6 is done
            try {
                if (version_compare($this->getDi()->store->get('db_version'), '6.0.0') < 0) {
                    unset($values['level']);
                    unset($values['channel']);
                    $this->insert($values, false);
                }
            } catch (Am_Exception_Db $e) {
                //just skip error log in this case
            }
        }
    }

    function handle(array $record): bool
    {
        $context = $record['context'];

        if (
            !empty($context['exception'])
            && is_object($context['exception'])
        ) {
            $e = $context['exception'];
            $trace = "Exception " . get_class($e) .':' . $e->getMessage() . " at line " . $e->getLine() . " in file " . $e->getFile() . PHP_EOL .

                self::backtraceToString($e->getTrace());
        } else {
            $trace = debug_backtrace(false);
            array_shift($trace); // remove ErrorLog->log entry
            array_shift($trace); // remove ErrorLog->log entry
            array_shift($trace); // remove ErrorLog->log entry
            $trace = self::backtraceToString($trace);
        }

        // duplicated here to avoid ANY incompat with table schema
        if (!$record['level'])
            $record['level'] = $this->_getNumLogLevel($record['level_name']);
        $values = [
            'time' => $this->getDi()->sqlDateTime,
            'user_id' => @$record['extra']['user_id'],
            'remote_addr' => @$record['extra']['remote_addr'],
            'url' => htmlentities($record['extra']['url'] ?? ''),
            'referrer' => htmlentities($record['extra']['referrer'] ?? ''),
            'error' => $record['message'],
            'trace' => $trace,
            'level' => $record['level'],
            'channel' => $record['channel'],
        ];
        try {
            $this->insert($values, false);
        } catch (Am_Exception_Db $e) { // do not break logging until admin-upgrade-db to v.6 is done
            try {
                if (version_compare($this->getDi()->store->get('db_version'), '6.0.0') < 0) {
                    unset($values['level']);
                    unset($values['channel']);
                    $this->insert($values, false);
                }
            } catch (Am_Exception_Db $e) {
                //just skip error log in this case
                return false;
            }
        }
        return true;
    }

    /**
     * Set monolog specific variables
     */
    function setLogHandlerConfig($minLevel = 200, $bubble = true)
    {
        $this->level = $minLevel;
        $this->bubble = $bubble;
    }

    /**
     * Checks whether the given record will be handled by this handler.
     *
     * This is mostly done for performance reasons, to avoid calling processors for nothing.
     *
     * Handlers should still check the record levels within handle(), returning false in isHandling()
     * is no guarantee that handle() will not be called, and isHandling() might not be called
     * for a given record.
     *
     * @param array $record Partial log record containing only a level key
     *
     * @return bool
     */
    public function isHandling(array $record): bool
    {
        return $record['level'] >= $this->level;
    }

    /**
     * Handles a set of records at once.
     *
     * @param array $records The records to handle (an array of record arrays)
     */
    public function handleBatch(array $records): void
    {
        foreach ($records as $record) {
            $this->handle($record);
        }
    }

    /**
     * Adds a processor in the stack.
     *
     * @param callable $callback
     * @return self
     */
    public function pushProcessor($callback)
    {
        if (!is_callable($callback)) {
            throw new \InvalidArgumentException('Processors must be valid callables (callback or object with an __invoke method), '.var_export($callback, true).' given');
        }
        array_unshift($this->processors, $callback);

        return $this;
    }

    /**
     * Removes the processor on top of the stack and returns it.
     *
     * @return callable
     */
    public function popProcessor()
    {
        if (!$this->processors) {
            throw new \LogicException('You tried to pop from an empty processor stack.');
        }

        return array_shift($this->processors);
    }

    /**
     * Sets the formatter.
     *
     * @param \Monolog\Formatter\FormatterInterface $formatter
     * @return self
     */
    public function setFormatter(\Monolog\Formatter\FormatterInterface $formatter)
    {
        $this->_formatter = $formatter;
        return $this;
    }

    /**
     * Gets the formatter.
     *
     * @return \Monolog\Formatter\FormatterInterface
     */
    public function getFormatter()
    {
        if (!$this->formatter) {
            $this->formatter = $this->getDefaultFormatter();
        }

        return $this->_formatter;
    }

    /**
     * Gets the default formatter.
     *
     * @return FormatterInterface
     */
    protected function getDefaultFormatter()
    {
        return new \Monolog\Formatter\LineFormatter();
    }

    public function close(): void
    {
        // TODO: Implement close() method.
    }
}