<?php
/**
 * For the full copyright and license information, please view the
 * docs/licenses/LICENSE.txt file that was distributed with this source code.
 */

use PrestaShopBundle\Form\Admin\Type\FormattedTextareaType;

/**
 * Class PrestaShopLoggerCore.
 */
class PrestaShopLoggerCore extends ObjectModel
{
    /**
     * List of log level types.
     */
    public const LOG_SEVERITY_LEVEL_DEBUG = 0;
    public const LOG_SEVERITY_LEVEL_INFORMATIVE = 1;
    public const LOG_SEVERITY_LEVEL_WARNING = 2;
    public const LOG_SEVERITY_LEVEL_ERROR = 3;
    public const LOG_SEVERITY_LEVEL_MAJOR = 4;

    /** @var int Log id */
    public $id_log;

    /** @var int Log severity */
    public $severity;

    /** @var int Error code */
    public $error_code;

    /** @var string Message */
    public $message;

    /** @var string Object type (eg. Order, Customer...) */
    public $object_type;

    /** @var int Object ID */
    public $object_id;

    /** @var int Employee ID */
    public $id_employee;

    /** @var string Object creation date */
    public $date_add;

    /** @var string Object last modification date */
    public $date_upd;

    /** @var int|null Shop ID */
    public $id_shop;

    /** @var int|null Shop group ID */
    public $id_shop_group;

    /** @var int|null Language ID */
    public $id_lang;

    /** @var bool In all shops */
    public $in_all_shops;

    /** @var string|null */
    public $hash;

    protected static int $minLevelInDb;

    /**
     * @see ObjectModel::$definition
     */
    public static $definition = [
        'table' => 'log',
        'primary' => 'id_log',
        'fields' => [
            'severity' => ['type' => self::TYPE_INT, 'validate' => 'isInt', 'required' => true],
            'error_code' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
            'message' => ['type' => self::TYPE_STRING, 'validate' => 'isString', 'required' => true, 'size' => FormattedTextareaType::LIMIT_MEDIUMTEXT_UTF8_MB4],
            'object_id' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
            'id_shop' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'allow_null' => true],
            'id_shop_group' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'allow_null' => true],
            'id_lang' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'allow_null' => true],
            'in_all_shops' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
            'id_employee' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
            'object_type' => ['type' => self::TYPE_STRING, 'validate' => 'isValidObjectClassName', 'size' => 32],
            'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
            'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
        ],
    ];

    protected static $is_present = [];

    /**
     * Send e-mail to the shop owner only if the minimal severity level has been reached.
     *
     * @param PrestaShopLogger $log
     */
    public static function sendByMail($log)
    {
        $config_severity = (int) Configuration::get('PS_LOGS_BY_EMAIL');
        if (!empty($config_severity) && $config_severity <= (int) $log->severity) {
            $to = array_map('trim', explode(',', Configuration::get('PS_LOGS_EMAIL_RECEIVERS')));
            $language = new Language((int) Configuration::get('PS_LANG_DEFAULT'));
            Mail::Send(
                (int) Configuration::get('PS_LANG_DEFAULT'),
                'log_alert',
                Context::getContext()->getTranslator()->trans(
                    'Log: You have a new alert from your shop',
                    [],
                    'Emails.Subject',
                    $language->locale
                ),
                [],
                $to
            );
        }
    }

    /**
     * add a log item to the database and send a mail if configured for this $severity.
     *
     * @param string $message the log message
     * @param int $severity
     * @param int $errorCode
     * @param string $objectType
     * @param int $objectId
     * @param bool $allowDuplicate if set to true, can log several time the same information (not recommended)
     *
     * @return bool true if succeed
     */
    public static function addLog($message, $severity = 1, $errorCode = null, $objectType = null, $objectId = null, $allowDuplicate = false, $idEmployee = null)
    {
        // Not all logs are relevant in DB so we filter them based on the configuration PS_MIN_LOGGER_LEVEL_IN_DB
        if ($severity < self::getMinimumLevelInDB()) {
            return false;
        }

        $log = new PrestaShopLogger();
        $log->severity = (int) $severity;
        $log->error_code = (int) $errorCode;
        $log->message = $message;
        $log->date_add = date('Y-m-d H:i:s');
        $log->date_upd = date('Y-m-d H:i:s');

        $context = Context::getContext();

        if ($idEmployee === null && isset($context->employee->id)) {
            $idEmployee = $context->employee->id;
        }

        if ($idEmployee !== null) {
            $log->id_employee = (int) $idEmployee;
        }

        if (!empty($objectType)) {
            $log->object_type = $objectType;
            if (!empty($objectId)) {
                $log->object_id = (int) $objectId;
            }
        }

        $log->id_lang = $context->language ? (int) $context->language->id : null;
        $log->in_all_shops = Shop::getContext() == Shop::CONTEXT_ALL;
        $log->id_shop = Shop::getContext() == Shop::CONTEXT_SHOP ? (int) $context->shop->getContextualShopId() : null;
        $log->id_shop_group = Shop::getContext() == Shop::CONTEXT_GROUP ? (int) $context->shop->getContextShopGroupID() : null;

        if ($objectType != 'MailerMessage') {
            PrestaShopLogger::sendByMail($log);
        }

        if ($allowDuplicate || !$log->isPresent()) {
            $res = $log->add();
            if ($res) {
                self::$is_present[$log->getHash()] = isset(self::$is_present[$log->getHash()]) ? self::$is_present[$log->getHash()] + 1 : 1;

                return true;
            }
        }

        return false;
    }

    /**
     * @return string hash
     */
    public function getHash()
    {
        if (empty($this->hash)) {
            $this->hash = md5(
                $this->message .
                $this->severity .
                $this->error_code .
                $this->object_type .
                $this->object_id .
                $this->id_shop .
                $this->id_shop_group .
                $this->id_lang .
                $this->in_all_shops
            );
        }

        return $this->hash;
    }

    public static function eraseAllLogs()
    {
        return Db::getInstance()->execute('TRUNCATE TABLE ' . _DB_PREFIX_ . 'log');
    }

    /**
     * check if this log message already exists in database.
     *
     * @return bool true if exists
     */
    protected function isPresent()
    {
        if (!isset(self::$is_present[md5($this->message)])) {
            self::$is_present[$this->getHash()] = Db::getInstance()->getValue(
                (new DbQuery())
                    ->select('COUNT(*)')
                    ->from('log', 'l')
                    ->where('message = "' . pSQL($this->message) . '"')
                    ->where('severity = ' . (int) $this->severity)
                    ->where('error_code = ' . (int) $this->error_code)
                    ->where('object_type = "' . pSQL($this->object_type) . '"')
                    ->where('object_id = ' . (int) $this->object_id)
                    ->where('id_shop = ' . (int) $this->id_shop)
                    ->where('id_shop_group = ' . (int) $this->id_shop_group)
                    ->where('id_lang = ' . (int) $this->id_lang)
                    ->where('in_all_shops = ' . (int) $this->in_all_shops)
            );
        }

        return self::$is_present[$this->getHash()];
    }

    protected static function getMinimumLevelInDB(): int
    {
        if (!isset(self::$minLevelInDb)) {
            try {
                self::$minLevelInDb = (int) Configuration::get('PS_MIN_LOGGER_LEVEL_IN_DB', null, null, null, self::LOG_SEVERITY_LEVEL_INFORMATIVE);
            } catch (Throwable) {
                self::$minLevelInDb = self::LOG_SEVERITY_LEVEL_INFORMATIVE;
            }
        }

        return self::$minLevelInDb;
    }
}
